# _ _ _ _____ _ _ _____ _ _ ___ ___ _ __
# /_\ | | |_ _| |_ (_)_ _ __ _ __|_ _|_ _| | |__ / __| \| |/ /
# / _ \| | | | | | ' \| | ' \/ _` (_-< | |/ _` | | / / \__ \ |) | ' <
# /_/ \_\_|_| |_| |_||_|_|_||_\__, /__/ |_|\__,_|_|_\_\ |___/___/|_|\_\
# |___/
#
# Copyright 2017 AllThingsTalk
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
__all__ = ['Device']
import datetime
from dateutil.parser import parse as parse_date
import json
import copy
from . import assets
# inspired by https://github.com/django/django/blob/master/django/db/models/base.py
class DeviceBase(type):
def __new__(cls, name, bases, attrs):
super_new = super().__new__
# Only perform for Device subclases (not Device class itself)
parents = [b for b in bases if isinstance(b, DeviceBase)]
if not parents:
return super_new(cls, name, bases, attrs)
new_attrs = {'__module__': attrs.pop('__module__')}
# Python 3.6 support: http://stackoverflow.com/questions/41343263
classcell = attrs.pop('__classcell__', None)
if classcell is not None:
new_attrs['__classcell__'] = classcell
new_class = super_new(cls, name, bases, new_attrs)
#
# Class enhancements
#
# Message handlers for state / feed / command / event
new_class._handlers = {}
new_class.state = DeviceBase.HandlerDecoratorCollection(new_class, 'state')
new_class.feed = DeviceBase.HandlerDecoratorCollection(new_class, 'feed')
new_class.command = DeviceBase.HandlerDecoratorCollection(new_class, 'command')
new_class.event = DeviceBase.HandlerDecoratorCollection(new_class, 'event')
# Asset transformations
for name, asset in attrs.items():
if isinstance(asset, assets.Asset):
# Configure asset name from variable name
if not asset.name:
asset.name = name
if not asset.title:
asset.title = name.capitalize()
asset._internal_id = name
# Create the actuation decorator
new_class.state._add_asset(asset)
new_class.feed._add_asset(asset)
new_class.command._add_asset(asset)
new_class.event._add_asset(asset)
new_class._assets = [value for name, value in attrs.items()
if isinstance(value, assets.Asset)]
return new_class
class HandlerDecoratorCollection:
def __init__(self, device_class, stream):
device_class._handlers[stream] = {}
self._stream = stream
self._device_class = device_class
self._assets = {}
def _add_asset(self, asset):
def decorator(fn):
self._device_class._handlers[self._stream][asset.name] = fn
return fn
self._assets[asset._internal_id] = decorator
def __getattr__(self, internal_id):
if internal_id in self._assets:
return self._assets[internal_id]
else:
return AttributeError
[docs]class Device(metaclass=DeviceBase):
"""Device contains information about assets. It maps to AllThingsTalk
Platform device resources."""
[docs] def __init__(self, *, client=None, id=None, connect=True,
overwrite_assets=False, **kwargs):
"""Initializes the device
:param Client client: The client used to interface with the platform
:param str id: Device resource id. If supplied, the device will be mapped to the device resource. If None, an attempt will be made to create the device.
:param bool connect: If ``True``, the device should connect to the cloud immediately.
:param bool overwrite_assets: If ``True``, asset mismatch between the Platform and device definition will be resolved by configuring local assets on the Platform. If ``False``, AssetMismatchException will be raised.
:raises AssetMismatchException: if asset mismatch is found between the existing asset on the Platform and an asset definition, and overwrite_assets is ``False``
"""
self._connected = False
self.id = id
self.client = client
self.overwrite_assets = overwrite_assets
self.assets = {asset._internal_id: copy.copy(asset) for asset in self._assets}
def make_get_asset(asset):
def getter(self):
return self.client.get_asset_state(self.id, asset.name)
return getter
def make_set_asset(asset):
def setter(self, value):
if self._connected:
self.client.publish_asset_state(self.id, asset.name, value)
else:
raise RuntimeError('Device not started.')
return setter
for asset in self.assets.values():
asset_property = property(
make_get_asset(asset), make_set_asset(asset), None,
asset.description or asset.name or asset._internal_id)
setattr(type(self), asset._internal_id, asset_property)
if connect and client:
self.connect()
[docs] def connect(self, *, client=None, id=None, overwrite_assets=None):
"""Connects to the device to AllThingsTalk Platform. The default
:class:`~allthingstalk.Client` calls this method automatically.
:param Client client: The client used to interface with the platform
:param str id: Device resource id. If supplied, the device will be mapped to the device resource. If None, an attempt will be made to create the device.
:param bool overwrite_assets: If ``True``, asset mismatch between the Platform and device definition will be resolved by configuring local assets on the Platform. If ``False``, AssetMismatchException will be raised.
:raises AssetMismatchException: if asset mismatch is found between the existing asset on the Platform and an asset definition, and overwrite_assets is ``False``
"""
if id is not None:
self.id = id
if client is not None:
self.client = client
if overwrite_assets is not None:
self.overwrite_assets = overwrite_assets
if not self.id:
raise NotImplementedError('Device creation not implemented.')
cloud_assets = {asset.name: asset for asset in self.client.get_assets(self.id)}
for asset in self.assets.values():
name = asset.name
if name in cloud_assets:
cloud_asset = cloud_assets[name]
else:
cloud_asset = self.client.create_asset(self.id, asset)
asset.id = cloud_asset.id
asset.thing_id = cloud_asset.thing_id
self.client._attach_device(self)
self._connected = True
def _on_message(self, stream, asset_name, message):
if asset_name in self._handlers[stream]:
msg = json.loads(message.decode('utf-8'))
if isinstance(msg, dict):
msg = {k.lower(): v for k, v in msg.items()}
else:
msg = {'value': msg}
if 'at' in msg and msg['at'] is not None:
at = parse_date(msg['at'])
else:
at = datetime.datetime.utcnow()
value = msg['value']
self._handlers[stream][asset_name](self, value, at)