Source code for allthingstalk.devices

#    _   _ _ _____ _    _              _____     _ _     ___ ___  _  __
#   /_\ | | |_   _| |_ (_)_ _  __ _ __|_   _|_ _| | |__ / __|   \| |/ /
#  / _ \| | | | | | ' \| | ' \/ _` (_-< | |/ _` | | / / \__ \ |) | ' <
# /_/ \_\_|_| |_| |_||_|_|_||_\__, /__/ |_|\__,_|_|_\_\ |___/___/|_|\_\
#                             |___/
#
# 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)