def __init__(self, client, lid, guid, epId): self.__client = client self.__lid = Validation.lid_check_convert(lid) self.__guid = Validation.guid_check_convert(guid) self.__epId = Validation.guid_check_convert(epId) # # Keep track of newly created points & subs (the requests for which originated from current agent) self.__new_feeds = ThreadSafeDict() self.__new_controls = ThreadSafeDict() self.__new_subs = ThreadSafeDict()
def __init__(self, client, lid, guid, epId): super(Thing, self).__init__(client, guid) self.__lid = Validation.lid_check_convert(lid) self.__epId = Validation.guid_check_convert(epId, allow_none=True) # Keep track of newly created points & subs (the requests for which originated from current agent) self.__new_feeds = ThreadSafeDict() self.__new_controls = ThreadSafeDict() self.__new_subs = ThreadSafeDict()
class Thing(object): # pylint: disable=too-many-public-methods """Thing class """ def __init__(self, client, lid, guid, epId): self.__client = client self.__lid = Validation.lid_check_convert(lid) self.__guid = Validation.guid_check_convert(guid) self.__epId = Validation.guid_check_convert(epId) # # Keep track of newly created points & subs (the requests for which originated from current agent) self.__new_feeds = ThreadSafeDict() self.__new_controls = ThreadSafeDict() self.__new_subs = ThreadSafeDict() @property def guid(self): """The Globally Unique ID of this Thing. In the 8-4-4-4-12 format """ return hex_to_uuid(self.__guid) @property def lid(self): """The local id of this Thing. This is unique to you on this container. Think of it as a nickname for the Thing """ return self.__lid @property def agent_id(self): """Agent id (aka epId) with which this Thing is associated. None indicates this Thing is not assigned to any agent. The following actions can only be performed with a Thing if operating in its associated agent: - Receive feed data from feeds the Thing is following - Share feed data for feeds this Thing owns - Receive control requests for controls the Thing owns - Perform ask/tell on a control this Thing is attached to Attempting to perform the above actions from another agent will result in either a local (e.g. ValueError) or remote ([IOTException](./Exceptions.m.html#IoticAgent.IOT.Exceptions.IOTException)) exception to be raised. """ return self.__epId def __list(self, foc, pid=None, limit=500, offset=0): logger.info("__list(foc=\"%s\", limit=%s, offset=%s) [lid=%s]", foc_to_str(foc), limit, offset, self.__lid) if pid is None: evt = self.__client._request_point_list(foc, self.__lid, limit=limit, offset=offset) else: evt = self.__client._request_point_list_detailed(foc, self.__lid, pid) evt.wait(self.__client.sync_timeout) self.__client._except_if_failed(evt) return evt.payload def list_feeds(self, pid=None, limit=500, offset=0): """List `all` the feeds on this Thing. Detailed list for a particular feed if feed id `pid` provided. Returns QAPI list function payload Raises [IOTException](./Exceptions.m.html#IoticAgent.IOT.Exceptions.IOTException) containing the error if the infrastructure detects a problem Raises [LinkException](../Core/AmqpLink.m.html#IoticAgent.Core.AmqpLink.LinkException) if there is a communications problem between you and the infrastructure `pid` (optional) (string) Local id of the feed for which you want the detailed listing. `limit` (optional) (integer) Return this many Point details `offset` (optional) (integer) Return Point details starting at this offset """ return self.__list(R_FEED, pid=pid, limit=limit, offset=offset)['feeds'] def list_controls(self, pid=None, limit=500, offset=0): """List `all` the controls on this Thing. Detailed list for a particular control if control id `pid` provided. Returns QAPI list function payload Raises [IOTException](./Exceptions.m.html#IoticAgent.IOT.Exceptions.IOTException) containing the error if the infrastructure detects a problem Raises [LinkException](../Core/AmqpLink.m.html#IoticAgent.Core.AmqpLink.LinkException) if there is a communications problem between you and the infrastructure `pid` (optional) (string) Local id of the control for which you want the detailed listing. `limit` (optional) (integer) Return this many Point details `offset` (optional) (integer) Return Point details starting at this offset """ return self.__list(R_CONTROL, pid=pid, limit=limit, offset=offset)['controls'] def set_public(self, public=True): """Sets your Thing to be public to all. If `public=True`. This means the tags, label and description of your Thing are now searchable by anybody, along with its location and the units of any values on any Points. If `public=False` the metadata of your Thing is no longer searchable. Raises [IOTException](./Exceptions.m.html#IoticAgent.IOT.Exceptions.IOTException) containing the error if the infrastructure detects a problem Raises [LinkException](../Core/AmqpLink.m.html#IoticAgent.Core.AmqpLink.LinkException) if there is a communications problem between you and the infrastructure `public` (optional) (boolean) Whether (or not) to allow your Thing's metadata to be searched by anybody """ logger.info("set_public(public=%s) [lid=%s]", public, self.__lid) evt = self.__client._request_entity_meta_setpublic(self.__lid, public) evt.wait(self.__client.sync_timeout) self.__client._except_if_failed(evt) def rename(self, new_lid): """Rename the Thing. `ADVANCED USERS ONLY` This can be confusing. You are changing the local id of a Thing to `new_lid`. If you create another Thing using the "old_lid", the system will oblige, but it will be a completely _new_ Thing. Raises [IOTException](./Exceptions.m.html#IoticAgent.IOT.Exceptions.IOTException) containing the error if the infrastructure detects a problem Raises [LinkException](../Core/AmqpLink.m.html#IoticAgent.Core.AmqpLink.LinkException) if there is a communications problem between you and the infrastructure `new_lid` (required) (string) the new local identifier of your Thing """ logger.info("rename(new_lid=\"%s\") [lid=%s]", new_lid, self.__lid) evt = self.__client._request_entity_rename(self.__lid, new_lid) evt.wait(self.__client.sync_timeout) self.__client._except_if_failed(evt) self.__lid = new_lid self.__client._notify_thing_lid_change(self.__lid, new_lid) def reassign(self, new_epid): """Reassign the Thing from one agent to another. `ADVANCED USERS ONLY` This will lead to any local instances of a Thing being rendered useless. They won't be able to receive control requests, feed data or to share any feeds as they won't be in this agent. Raises [IOTException](./Exceptions.m.html#IoticAgent.IOT.Exceptions.IOTException) containing the error if the infrastructure detects a problem Raises [LinkException](../Core/AmqpLink.m.html#IoticAgent.Core.AmqpLink.LinkException) if there is a communications problem between you and the infrastructure `new_epid` (required) (string) the new agent id to which your Thing should be assigned. If None, current agent will be chosen. If False, existing agent will be unassigned. """ logger.info("reassign(new_epid=\"%s\") [lid=%s]", new_epid, self.__lid) evt = self.__client._request_entity_reassign(self.__lid, new_epid) evt.wait(self.__client.sync_timeout) self.__client._except_if_failed(evt) def create_tag(self, tags, lang=None): """Create tags for a Thing in the language you specify. Tags can only contain alphanumeric (unicode) characters and the underscore. Tags will be stored lower-cased. Raises [IOTException](./Exceptions.m.html#IoticAgent.IOT.Exceptions.IOTException) containing the error if the infrastructure detects a problem Raises [LinkException](../Core/AmqpLink.m.html#IoticAgent.Core.AmqpLink.LinkException) if there is a communications problem between you and the infrastructure `tags` (mandatory) (list) - the list of tags you want to add to your Thing, e.g. `["garden", "soil"]` `lang` (optional) (string) The two-character ISO 639-1 language code to use for your label. None means use the default language for your agent. See [Config](./Config.m.html#IoticAgent.IOT.Config.Config.__init__) """ if isinstance(tags, str): tags = [tags] evt = self.__client._request_entity_tag_create(self.__lid, tags, lang, delete=False) evt.wait(self.__client.sync_timeout) self.__client._except_if_failed(evt) def delete_tag(self, tags, lang=None): """Delete tags for a Thing in the language you specify. Case will be ignored and any tags matching lower-cased will be deleted. Raises [IOTException](./Exceptions.m.html#IoticAgent.IOT.Exceptions.IOTException) containing the error if the infrastructure detects a problem Raises [LinkException](../Core/AmqpLink.m.html#IoticAgent.Core.AmqpLink.LinkException) if there is a communications problem between you and the infrastructure `tags` (mandatory) (list) - the list of tags you want to delete from your Thing, e.g. `["garden", "soil"]` `lang` (optional) (string) The two-character ISO 639-1 language code to use for your label. None means use the default language for your agent. See [Config](./Config.m.html#IoticAgent.IOT.Config.Config.__init__) """ if isinstance(tags, str): tags = [tags] evt = self.__client._request_entity_tag_delete(self.__lid, tags, lang) evt.wait(self.__client.sync_timeout) self.__client._except_if_failed(evt) def list_tag(self, limit=500, offset=0): """List `all` the tags for this Thing Returns tag dictionary of lists of tags keyed by language. As below #!python { "en": [ "mytag1", "mytag2" ], "de": [ "ein_name", "nochein_name" ] } - OR... Raises [IOTException](./Exceptions.m.html#IoticAgent.IOT.Exceptions.IOTException) containing the error if the infrastructure detects a problem Raises [LinkException](../Core/AmqpLink.m.html#IoticAgent.Core.AmqpLink.LinkException) if there is a communications problem between you and the infrastructure `limit` (optional) (integer) Return at most this many tags `offset` (optional) (integer) Return tags starting at this offset """ evt = self.__client._request_entity_tag_list(self.__lid, limit=limit, offset=offset) evt.wait(self.__client.sync_timeout) self.__client._except_if_failed(evt) return evt.payload['tags'] def get_meta(self): """Get the metadata object for this Thing Returns a [ThingMeta](ThingMeta.m.html#IoticAgent.IOT.ThingMeta.ThingMeta) object Raises `RuntimeError` if RDFLib is not installed or available """ if ThingMeta is None: raise RuntimeError("ThingMeta not available") rdf = self.get_meta_rdf(fmt='n3') return ThingMeta(self, rdf, self.__client.default_lang, fmt='n3') def get_meta_rdf(self, fmt='n3'): """Get the metadata for this Thing in rdf fmt Advanced users who want to manipulate the RDF for this Thing directly without the [ThingMeta](ThingMeta.m.html#IoticAgent.IOT.ThingMeta.ThingMeta) helper object Returns the RDF in the format you specify. - OR - Raises [IOTException](./Exceptions.m.html#IoticAgent.IOT.Exceptions.IOTException) containing the error if the infrastructure detects a problem Raises [LinkException](../Core/AmqpLink.m.html#IoticAgent.Core.AmqpLink.LinkException) if there is a communications problem between you and the infrastructure `fmt` (optional) (string) The format of RDF you want returned. Valid formats are: "xml", "n3", "turtle" """ evt = self.__client._request_entity_meta_get(self.__lid, fmt=fmt) evt.wait(self.__client.sync_timeout) self.__client._except_if_failed(evt) return evt.payload['meta'] def set_meta_rdf(self, rdf, fmt='n3'): """Set the metadata for this Thing in RDF fmt Advanced users who want to manipulate the RDF for this Thing directly without the [ThingMeta](ThingMeta.m.html#IoticAgent.IOT.ThingMeta.ThingMeta) helper object Raises [IOTException](./Exceptions.m.html#IoticAgent.IOT.Exceptions.IOTException) containing the error if the infrastructure detects a problem Raises [LinkException](../Core/AmqpLink.m.html#IoticAgent.Core.AmqpLink.LinkException) if there is a communications problem between you and the infrastructure `fmt` (optional) (string) The format of RDF you have sent. Valid formats are: "xml", "n3", "turtle" """ evt = self.__client._request_entity_meta_set(self.__lid, rdf, fmt=fmt) evt.wait(self.__client.sync_timeout) self.__client._except_if_failed(evt) def get_feed(self, pid): """Get the details of a newly created feed. This only applies to asynchronous creation of feeds and the new feed instance can only be retrieved once. `NOTE` - Destructive Read. Once you've called get_feed once, any further calls will raise a `KeyError` Returns a [Point](Point.m.html#IoticAgent.IOT.Point.Point) object, which corresponds to the cached entry for this local point id `pid` (required) (string) Point id - local identifier of your feed. Raises `KeyError` if the feed has not been newly created (or has already been retrieved by a previous call) """ with self.__new_feeds: try: return self.__new_feeds.pop(pid) except KeyError as ex: raise_from(KeyError('Feed %s not know as new' % pid), ex) def get_control(self, pid): """Get the details of a newly created control. This only applies to asynchronous creation of feeds and the new control instance can only be retrieved once. `NOTE` - Destructive Read. Once you've called get_control once, any further calls will raise a `KeyError` Returns a [Point](Point.m.html#IoticAgent.IOT.Point.Point) object, which corresponds to the cached entry for this local point id `pid` (required) (string) local identifier of your control. Raises `KeyError` if the control has not been newly created (or has already been retrieved by a previous call) """ with self.__new_controls: try: return self.__new_controls.pop(pid) except KeyError as ex: raise_from(KeyError('Control %s not know as new' % pid), ex) def __create_point(self, foc, pid, control_cb=None): evt = self.__client._request_point_create(foc, self.__lid, pid, control_cb=control_cb) evt.wait(self.__client.sync_timeout) self.__client._except_if_failed(evt) store = self.__new_feeds if foc == R_FEED else self.__new_controls try: with store: return store.pop(pid) except KeyError as ex: raise raise_from(IOTClientError('%s %s (from %s) not in cache (post-create)' % (foc_to_str(foc), pid, self.__lid)), ex) def __create_point_async(self, foc, pid, control_cb=None): return self.__client._request_point_create(foc, self.__lid, pid, control_cb=control_cb) def create_feed(self, pid): """Create a new Feed for this Thing with a local point id (pid). Returns a new [Point](Point.m.html#IoticAgent.IOT.Point.Point) object, or the existing one, if the Point already exists Raises [IOTException](./Exceptions.m.html#IoticAgent.IOT.Exceptions.IOTException) containing the error if the infrastructure detects a problem Raises [LinkException](../Core/AmqpLink.m.html#IoticAgent.Core.AmqpLink.LinkException) if there is a communications problem between you and the infrastructure `pid` (required) (string) local id of your Feed """ logger.info("create_feed(pid=\"%s\") [lid=%s]", pid, self.__lid) return self.__create_point(R_FEED, pid) def create_feed_async(self, pid): logger.info("create_feed_async(pid=\"%s\") [lid=%s]", pid, self.__lid) return self.__create_point_async(R_FEED, pid) def create_control(self, pid, control_cb): """Create a control for this Thing with a local point id (pid) and a control request feedback Returns a new [Point](Point.m.html#IoticAgent.IOT.Point.Point) object or the existing one if the Point already exists Raises [IOTException](./Exceptions.m.html#IoticAgent.IOT.Exceptions.IOTException) containing the error if the infrastructure detects a problem Raises [LinkException](../Core/AmqpLink.m.html#IoticAgent.Core.AmqpLink.LinkException) if there is a communications problem between you and the infrastructure `pid` (required) (string) local id of your Control `control_cb` (required) (function reference) callback function to invoke on receipt of a control request. The callback receives a single dict argument, with keys of: #!python 'data' # (decoded or raw bytes) 'mime' # (None, unless payload was not decoded and has a mime type) 'subId' # (the global id of the associated subscripion) 'entityLid' # (local id of the Thing to which the control belongs) 'lid' # (local id of control) 'confirm' # (whether a confirmation is expected) 'requestId' # (required for sending confirmation) """ logger.info("create_control(pid=\"%s\", control_cb=%s) [lid=%s]", pid, control_cb, self.__lid) return self.__create_point(R_CONTROL, pid, control_cb=control_cb) def create_control_async(self, pid, control_cb): logger.info("create_control_async(pid=\"%s\", control_cb=%s) [lid=%s]", pid, control_cb, self.__lid) return self.__create_point_async(R_CONTROL, pid, control_cb=control_cb) def __delete_point(self, foc, pid): evt = self.__client._request_point_delete(foc, self.__lid, pid) evt.wait(self.__client.sync_timeout) self.__client._except_if_failed(evt) def __delete_point_async(self, foc, pid): return self.__client._request_point_delete(foc, self.__lid, pid) def delete_feed(self, pid): """Delete a feed, identified by its local id. Raises [IOTException](./Exceptions.m.html#IoticAgent.IOT.Exceptions.IOTException) containing the error if the infrastructure detects a problem Raises [LinkException](../Core/AmqpLink.m.html#IoticAgent.Core.AmqpLink.LinkException) if there is a communications problem between you and the infrastructure `pid` (required) (string) local identifier of your feed you want to delete """ logger.info("delete_feed(pid=\"%s\") [lid=%s]", pid, self.__lid) return self.__delete_point(R_FEED, pid) def delete_feed_async(self, pid): logger.info("delete_feed_async(pid=\"%s\") [lid=%s]", pid, self.__lid) return self.__delete_point_async(R_FEED, pid) def delete_control(self, pid): """Delete a control, identified by its local id. Raises [IOTException](./Exceptions.m.html#IoticAgent.IOT.Exceptions.IOTException) containing the error if the infrastructure detects a problem Raises [LinkException](../Core/AmqpLink.m.html#IoticAgent.Core.AmqpLink.LinkException) if there is a communications problem between you and the infrastructure `pid` (required) (string) local identifier of your control you want to delete """ logger.info("delete_control(pid=\"%s\") [lid=%s]", pid, self.__lid) return self.__delete_point(R_CONTROL, pid) def delete_control_async(self, pid): logger.info("delete_control_async(pid=\"%s\") [lid=%s]", pid, self.__lid) return self.__delete_point_async(R_CONTROL, pid) def __get_sub(self, foc, gpid): # global if isinstance(gpid, string_types): gpid = uuid_to_hex(gpid) # not local elif not (isinstance(gpid, Sequence) and len(gpid) == 2): raise ValueError('gpid must be string or two-element tuple') try: sub = self.__new_subs.pop((foc, gpid)) except KeyError as ex: raise_from(KeyError('Remote%s subscription %s not know as new' % (foc_to_str(foc).capitalize(), gpid)), ex) if sub is None: raise ValueError('Either subscription not complete yet or point %s is of opposite type. (In which case' ' call %s instead' % (gpid, 'attach()' if foc == R_FEED else 'follow()')) return sub def get_remote_feed(self, gpid): """Retrieve `RemoteFeed` instance for a follow. This only applies to asynchronous follow requests and the new `RemoteFeed` instance can only be retrieved once. `NOTE` - Destructive Read. Once you've called get_remote_feed once, any further calls will raise a `KeyError` Raises `KeyError` if the follow-subscription has not been newly created (or has already been retrieved by a previous call) Raises `ValueError` if the followed Point is actually a control instead of a feed, or if the subscription has not completed yet. """ return self.__get_sub(R_FEED, gpid) def get_remote_control(self, gpid): """Retrieve `RemoteControl` instance for an attach. This only applies to asynchronous attach requests and the new `RemoteControl` instance can only be retrieved once. `NOTE` - Destructive Read. Once you've called get_remote_control once, any further calls will raise a `KeyError` Raises `KeyError` if the attach-subscription has not been newly created (or has already been retrieved by a previous call) Raises `ValueError` if the followed Point is actually a feed instead of a control, or if the subscription has not completed yet.""" return self.__get_sub(R_CONTROL, gpid) @contextmanager def __sub_add_reference(self, key): """Used by __sub_make_request to save reference for pending sub request""" new_subs = self.__new_subs with new_subs: # don't allow multiple subscription requests to overwrite internal reference if key in new_subs: raise ValueError('subscription for given args pending: %s', key) new_subs[key] = None try: yield except: # don't preserve reference if request creation failed with new_subs: new_subs.pop(key, None) def __sub_make_request(self, foc, gpid, callback): """Make right subscription request depending on whether local or global - used by __sub*""" # global if isinstance(gpid, string_types): gpid = uuid_to_hex(gpid) with self.__sub_add_reference((foc, gpid)): return self.__client._request_sub_create(self.__lid, foc, gpid, callback=callback) # local elif isinstance(gpid, Sequence) and len(gpid) == 2: with self.__sub_add_reference((foc, tuple(gpid))): return self.__client._request_sub_create_local(self.__lid, foc, *gpid, callback=callback) else: raise ValueError('gpid must be string or two-element tuple') def __sub(self, foc, gpid, callback=None): logger.info("__sub(foc=%s, gpid=\"%s\", callback=%s) [lid=%s]", foc_to_str(foc), gpid, callback, self.__lid) evt = self.__sub_make_request(foc, gpid, callback) evt.wait(self.__client.sync_timeout) self.__client._except_if_failed(evt) try: return self.__get_sub(foc, gpid) except KeyError as ex: raise raise_from(IOTClientError('Subscription for %s (from %s) not in cache (post-create)' % gpid, self.__lid), ex) def __sub_async(self, foc, gpid, callback=None): logger.info("__sub_async(foc=%s, gpid=\"%s\", callback=%s) [lid=%s]", foc_to_str(foc), gpid, callback, self.__lid) return self.__sub_make_request(foc, gpid, callback) def follow(self, gpid, callback=None): """Create a subscription (i.e. follow) a Feed/Point with a global point id (gpid) and a feed data callback Returns a new [RemoteFeed](RemoteFeed.m.html#IoticAgent.IOT.RemoteFeed.RemoteFeed) object or the existing one if the subscription already exists - OR - Raises [IOTException](./Exceptions.m.html#IoticAgent.IOT.Exceptions.IOTException) containing the error if the infrastructure detects a problem Raises [LinkException](../Core/AmqpLink.m.html#IoticAgent.Core.AmqpLink.LinkException) if there is a communications problem between you and the infrastructure `gpid` (required) (uuid) global id of the Point you want to follow `-OR-` `gpid` (required) (lid,pid) tuple of `(thing_localid, point_localid)` for local subscription `callback` (optional) (function reference) callback function to invoke on receipt of feed data. The callback receives a single dict argument, with keys of: #!python 'data' # (decoded or raw bytes) 'mime' # (None, unless payload was not decoded and has a mime type) 'pid' # (the global id of the feed from which the data originates) """ return self.__sub(R_FEED, gpid, callback=callback) def follow_async(self, gpid, callback=None): return self.__sub_async(R_FEED, gpid, callback=callback) def attach(self, gpid): """Create a subscription (i.e. attach) to a Control-Point with a global point id (gpid) and a feed data callback Returns a new [RemoteControl](RemoteControl.m.html#IoticAgent.IOT.RemoteControl.RemoteControl) object or the existing one if the subscription already exists Raises [IOTException](./Exceptions.m.html#IoticAgent.IOT.Exceptions.IOTException) containing the error if the infrastructure detects a problem Raises [LinkException](../Core/AmqpLink.m.html#IoticAgent.Core.AmqpLink.LinkException) if there is a communications problem between you and the infrastructure `gpid` (required) (uuid) global id of the Point to which you want to attach `-OR-` `gpid` (required) (lid,pid) tuple of `(thing_localid, point_localid)` for local subscription """ return self.__sub(R_CONTROL, gpid) def attach_async(self, gpid): return self.__sub_async(R_CONTROL, gpid) def __sub_delete(self, subid): if isinstance(subid, (RemoteFeed, RemoteControl)): subid = subid.subid evt = self.__client._request_sub_delete(subid) evt.wait(self.__client.sync_timeout) self.__client._except_if_failed(evt) def unfollow(self, subid): """Remove a subscription of a Feed with a global subscription id (gpid) Raises [IOTException](./Exceptions.m.html#IoticAgent.IOT.Exceptions.IOTException) containing the error if the infrastructure detects a problem Raises [LinkException](../Core/AmqpLink.m.html#IoticAgent.Core.AmqpLink.LinkException) if there is a communications problem between you and the infrastructure `subid` (required) `Either` (uuid) global id of the subscription you want to delete `or` (object) The instance of a RemoteFeed object corresponding to the feed you want to cease following. """ return self.__sub_delete(subid) def unattach(self, subid): """Remove a subscription of a control with a global subscription id (gpid) Raises [IOTException](./Exceptions.m.html#IoticAgent.IOT.Exceptions.IOTException) containing the error if the infrastructure detects a problem Raises [LinkException](../Core/AmqpLink.m.html#IoticAgent.Core.AmqpLink.LinkException) if there is a communications problem between you and the infrastructure `subid` (required) ( `Either` (uuid) global id of the subscription you want to delete `or` (object) The instance of a RemoteControl object corresponding to the control you want to cease being able to actuate. """ return self.__sub_delete(subid) def list_connections(self, limit=500, offset=0): """List Points to which this Things is subscribed. I.e. list all the Points this Thing is following and controls it's attached to Returns subscription list e.g. #!python { "<Subscription GUID 1>": { "id": "<Control GUID>", "entityId": "<Control's Thing GUID>", "type": 3 # R_CONTROL from IoticAgent.Core.Const }, "<Subscription GUID 2>": { "id": "<Feed GUID>", "entityId": "<Feed's Thing GUID>", "type": 2 # R_FEED from IoticAgent.Core.Const } Raises [IOTException](./Exceptions.m.html#IoticAgent.IOT.Exceptions.IOTException) containing the error if the infrastructure detects a problem Raises [LinkException](../Core/AmqpLink.m.html#IoticAgent.Core.AmqpLink.LinkException) if there is a communications problem between you and the infrastructure Note: For Things following a Point see [list_followers](./Point.m.html#IoticAgent.IOT.Point.Point.list_followers) """ evt = self.__client._request_sub_list(self.__lid, limit=limit, offset=offset) evt.wait(self.__client.sync_timeout) self.__client._except_if_failed(evt) return evt.payload['subs'] def _cb_created(self, payload, duplicated): """Indirect callback (via Client) for point & subscription creation responses""" if payload[P_RESOURCE] in _POINT_TYPES: store = self.__new_feeds if payload[P_RESOURCE] == R_FEED else self.__new_controls with store: store[payload[P_LID]] = Point(self.__client, payload[P_RESOURCE], payload[P_ENTITY_LID], payload[P_LID], payload[P_ID]) logger.debug('Added %s: %s (for %s)', foc_to_str(payload[P_RESOURCE]), payload[P_LID], payload[P_ENTITY_LID]) elif payload[P_RESOURCE] == R_SUB: # local if P_POINT_ENTITY_LID in payload: key = (payload[P_POINT_TYPE], (payload[P_POINT_ENTITY_LID], payload[P_POINT_LID])) # global else: key = (payload[P_POINT_TYPE], payload[P_POINT_ID]) new_subs = self.__new_subs with new_subs: if key in new_subs: cls = RemoteFeed if payload[P_POINT_TYPE] == R_FEED else RemoteControl new_subs[key] = cls(self.__client, payload[P_ID], payload[P_POINT_ID]) else: logger.warning('Ignoring subscription creation for unexpected %s: %s', foc_to_str(payload[P_POINT_TYPE]), key[1]) else: logger.error('Resource creation of type %d unhandled', payload[P_RESOURCE]) def _cb_reassigned(self, payload): self.__epId = payload[P_EPID] logger.info('Thing %s reassigned to agent %s', self.__lid, self.__epId)
class Thing(Resource): # pylint: disable=too-many-public-methods """Thing class """ def __init__(self, client, lid, guid, epId): super(Thing, self).__init__(client, guid) self.__lid = Validation.lid_check_convert(lid) self.__epId = Validation.guid_check_convert(epId, allow_none=True) # Keep track of newly created points & subs (the requests for which originated from current agent) self.__new_feeds = ThreadSafeDict() self.__new_controls = ThreadSafeDict() self.__new_subs = ThreadSafeDict() def __str__(self): return '%s (thing, %s)' % (self.guid, self.__lid) def __hash__(self): # Why not just hash guid? Because Thing is used before knowing guid in some cases # Why not hash without guid? Because in two separate containers one could have identicial things # (if not taking guid into account) return hash(self.__lid) ^ hash(self.guid) def __eq__(self, other): return (isinstance(other, Thing) and self.guid == other.guid and self.__lid == other.__lid) @property def lid(self): """ The local id of this Thing. This is unique to you on this container. Think of it as a nickname for the Thing """ return self.__lid @property def agent_id(self): """ Agent id (aka epId) with which this Thing is associated. None indicates this Thing is not assigned to any agent. The following actions can only be performed with a Thing if operating in its associated agent: * Receive feed data from feeds the Thing is following * Share feed data for feeds this Thing owns * Receive control requests for controls the Thing owns * Perform ask/tell on a control this Thing is attached to Attempting to perform the above actions from another agent will result in either a local (e.g. ValueError) or remote exception to be raised(:doc:`IoticAgent.IOT.Exceptions`). """ return self.__epId def __list(self, foc, limit=500, offset=0): logger.info("__list(foc=\"%s\", limit=%s, offset=%s) [lid=%s]", foc_to_str(foc), limit, offset, self.__lid) evt = self._client._request_point_list(foc, self.__lid, limit=limit, offset=offset) self._client._wait_and_except_if_failed(evt) return evt.payload def list_feeds(self, limit=500, offset=0): """ List `all` the feeds on this Thing. Returns: QAPI list function payload Raises: IOTException: Infrastructure problem detected LinkException: Communications problem between you and the infrastructure Args: limit (integer, optional): Return this many Point details offset (integer, optional): Return Point details starting at this offset """ return self.__list(R_FEED, limit=limit, offset=offset)['feeds'] def list_controls(self, limit=500, offset=0): """ List `all` the controls on this Thing. Returns: QAPI list function payload Raises: IOTException: Infrastructure problem detected LinkException: Communications problem between you and the infrastructure Args: limit (integer, optional): Return this many Point details offset (integer, optional): Return Point details starting at this offset """ return self.__list(R_CONTROL, limit=limit, offset=offset)['controls'] def set_public(self, public=True): """ Sets your Thing to be public to all if `public=True`. This means the tags, label and description of your Thing are now searchable by anybody, along with its location and the units of any values on any Points. If `public=False` the metadata of your Thing is no longer searchable. Raises: IOTException: Infrastructure problem detected LinkException: Communications problem between you and the infrastructure Args: public (boolean, optional): Whether (or not) to allow your Thing's metadata to be searched by anybody """ logger.info("set_public(public=%s) [lid=%s]", public, self.__lid) evt = self._client._request_entity_meta_setpublic(self.__lid, public) self._client._wait_and_except_if_failed(evt) def rename(self, new_lid): """ Rename the Thing. **Advanced users only** This can be confusing. You are changing the local id of a Thing to `new_lid`. If you create another Thing using the "old_lid", the system will oblige, but it will be a completely **new** Thing. Raises: IOTException: Infrastructure problem detected LinkException: Communications problem between you and the infrastructure Args: new_lid (string): The new local identifier of your Thing """ logger.info("rename(new_lid=\"%s\") [lid=%s]", new_lid, self.__lid) evt = self._client._request_entity_rename(self.__lid, new_lid) self._client._wait_and_except_if_failed(evt) self.__lid = new_lid self._client._notify_thing_lid_change(self.__lid, new_lid) def reassign(self, new_epid): """ Reassign the Thing from one agent to another. **Advanced users only** This will lead to any local instances of a Thing being rendered useless. They won't be able to receive control requests, feed data or to share any feeds as they won't be in this agent. Raises: IOTException: Infrastructure problem detected LinkException: Communications problem between you and the infrastructure Args: new_epid (string): The new agent id to which your Thing should be assigned. If None, current agent will be chosen. If False, existing agent will be unassigned. """ logger.info("reassign(new_epid=\"%s\") [lid=%s]", new_epid, self.__lid) evt = self._client._request_entity_reassign(self.__lid, new_epid) self._client._wait_and_except_if_failed(evt) def create_tag(self, tags): """ Create tags for a Thing in the language you specify. Tags can only contain alphanumeric (unicode) characters and the underscore. Tags will be stored lower-cased. Raises: IOTException: Infrastructure problem detected LinkException: Communications problem between you and the infrastructure Args: tags (list): The list of tags you want to add to your Thing, e.g. `["garden", "soil"]` """ if isinstance(tags, str): tags = [tags] evt = self._client._request_entity_tag_update(self.__lid, tags, delete=False) self._client._wait_and_except_if_failed(evt) def delete_tag(self, tags): """ Delete tags for a Thing in the language you specify. Case will be ignored and any tags matching lower-cased will be deleted. Raises: IOTException: Infrastructure problem detected LinkException: Communications problem between you and the infrastructure Args: tags (list): The list of tags you want to delete from your Thing, e.g. `["garden", "soil"]` """ if isinstance(tags, str): tags = [tags] evt = self._client._request_entity_tag_update(self.__lid, tags, delete=True) self._client._wait_and_except_if_failed(evt) def list_tag(self, limit=500, offset=0): """List `all` the tags for this Thing Returns: Lists of tags, as below :: [ "mytag1", "mytag2" "ein_name", "nochein_name" ] Or: Raises: IOTException: Infrastructure problem detected LinkException: Communications problem between you and the infrastructure Args: limit (integer, optional): Return at most this many tags offset (integer, optional): Return tags starting at this offset """ evt = self._client._request_entity_tag_list(self.__lid, limit=limit, offset=offset) self._client._wait_and_except_if_failed(evt) return evt.payload['tags'] def get_meta(self): """ Get the metadata object for this Thing Returns: A :doc:`IoticAgent.IOT.ThingMeta` object """ rdf = self.get_meta_rdf(fmt='n3') return ThingMeta(self, rdf, self._client.default_lang, fmt='n3') def get_meta_rdf(self, fmt='n3'): """ Get the metadata for this Thing in rdf fmt. Advanced users who want to manipulate the RDF for this Thing directly without the :doc:`IoticAgent.IOT.ThingMeta` helper object. Returns: The RDF in the format you specify OR Raises: IOTException: Infrastructure problem detected LinkException: Communications problem between you and the infrastructure Args: fmt (string, optional): The format of RDF you want returned. Valid formats are: "xml", "n3", "turtle" """ evt = self._client._request_entity_meta_get(self.__lid, fmt=fmt) self._client._wait_and_except_if_failed(evt) return evt.payload['meta'] def set_meta_rdf(self, rdf, fmt='n3'): """ Set the metadata for this Thing in RDF fmt. Advanced users who want to manipulate the RDF for this Thing directly without the :doc:`IoticAgent.IOT.ThingMeta` helper object. Raises: IOTException: Infrastructure problem detected LinkException: Communications problem between you and the infrastructure Args: fmt (string, optional): The format of RDF you have sent. Valid formats are: "xml", "n3", "turtle" """ evt = self._client._request_entity_meta_set(self.__lid, rdf, fmt=fmt) self._client._wait_and_except_if_failed(evt) def get_feed(self, pid): """ Get the details of a newly created feed. This only applies to asynchronous creation of feeds and the new feed instance can only be retrieved once. Note: Destructive Read. Once you've called get_feed once, any further calls will raise a `KeyError` Returns: A :doc:`IoticAgent.IOT.Point` feed object, which corresponds to the cached entry for this local feed id. Args: pid (string): Point id - local identifier of your feed. Raises: KeyError: Feed has not been newly created (or has already been retrieved by a previous call) """ with self.__new_feeds: try: return self.__new_feeds.pop(pid) except KeyError as ex: raise_from(KeyError('Feed %s not know as new' % pid), ex) def get_control(self, pid): """ Get the details of a newly created control. This only applies to asynchronous creation of feeds and the new control instance can only be retrieved once. Note: Destructive Read. Once you've called get_control once, any further calls will raise a `KeyError` Returns: A :doc:`IoticAgent.IOT.Point` control object, which corresponds to the cached entry for this local control id Args: pid (string): Local identifier of your control. Raises: KeyError: The control has not been newly created (or has already been retrieved by a previous call) """ with self.__new_controls: try: return self.__new_controls.pop(pid) except KeyError as ex: raise_from(KeyError('Control %s not know as new' % pid), ex) def __create_point(self, foc, pid, control_cb=None, save_recent=0): evt = self.__create_point_async(foc, pid, control_cb=control_cb, save_recent=save_recent) self._client._wait_and_except_if_failed(evt) store = self.__new_feeds if foc == R_FEED else self.__new_controls try: with store: return store.pop(pid) except KeyError as ex: raise raise_from( IOTClientError('%s %s (from %s) not in cache (post-create)' % (foc_to_str(foc), pid, self.__lid)), ex) def __create_point_async(self, foc, pid, control_cb=None, save_recent=0): return self._client._request_point_create(foc, self.__lid, pid, control_cb=control_cb, save_recent=save_recent) def create_feed(self, pid, save_recent=0): """ Create a new Feed for this Thing with a local point id (pid). Returns: A new :doc:`IoticAgent.IOT.Point` feed object, or the existing one, if the Feed already exists Raises: IOTException: Infrastructure problem detected LinkException: Communications problem between you and the infrastructure Args: pid (string): Local id of your Feed save_recent (int, optional): How many shares to store for later retrieval. If not supported by container, this argument will be ignored. A value of zero disables this feature whilst a negative value requests the maximum sample store amount. """ logger.info("create_feed(pid=\"%s\") [lid=%s]", pid, self.__lid) return self.__create_point(R_FEED, pid, save_recent=save_recent) def create_feed_async(self, pid, save_recent=0): logger.info("create_feed_async(pid=\"%s\") [lid=%s]", pid, self.__lid) return self.__create_point_async(R_FEED, pid, save_recent=save_recent) def create_control(self, pid, callback, callback_parsed=None): """ Create a control for this Thing with a local point id (pid) and a control request feedback Returns: A new :doc:`IoticAgent.IOT.Point` control object or the existing one if the Control already exists Raises: IOTException: Infrastructure problem detected LinkException: Communications problem between you and the infrastructure Args: pid (string): Local id of your Control callback (function reference): Callback function to invoke on receipt of a control request. callback_parsed (function reference, optional): Callback function to invoke on receipt of control data. This is equivalent to `callback` except the dict includes the `parsed` key which holds the set of values in a :doc:`IoticAgent.IOT.Point` PointDataObject instance. If both `callback_parsed` and callback` have been specified, the former takes precedence and `callback` is only called if the point data could not be parsed according to its current value description. The `callback` receives a single dict argument, with keys of: :: 'data' # (decoded or raw bytes) 'mime' # (None, unless payload was not decoded and has a mime type) 'subId' # (the global id of the associated subscripion) 'entityLid' # (local id of the Thing to which the control belongs) 'lid' # (local id of control) 'confirm' # (whether a confirmation is expected) 'requestId' # (required for sending confirmation) Note: `callback_parsed` can only be used if `auto_encode_decode` is enabled for the client instance. """ logger.info("create_control(pid=\"%s\", control_cb=%s) [lid=%s]", pid, callback, self.__lid) if callback_parsed: callback = self._client._get_parsed_control_callback( callback_parsed, callback) return self.__create_point(R_CONTROL, pid, control_cb=callback) def create_control_async(self, pid, callback, callback_parsed=None): logger.info("create_control_async(pid=\"%s\", control_cb=%s) [lid=%s]", pid, callback, self.__lid) if callback_parsed: callback = self._client._get_parsed_control_callback( callback_parsed, callback) return self.__create_point_async(R_CONTROL, pid, control_cb=callback) def __delete_point(self, foc, pid): evt = self.__delete_point_async(foc, pid) self._client._wait_and_except_if_failed(evt) def __delete_point_async(self, foc, pid): return self._client._request_point_delete(foc, self.__lid, pid) def delete_feed(self, pid): """ Delete a feed, identified by its local id. Raises: IOTException: Infrastructure problem detected LinkException: Communications problem between you and the infrastructure Args: pid (string): Local identifier of your feed you want to delete """ logger.info("delete_feed(pid=\"%s\") [lid=%s]", pid, self.__lid) return self.__delete_point(R_FEED, pid) def delete_feed_async(self, pid): logger.info("delete_feed_async(pid=\"%s\") [lid=%s]", pid, self.__lid) return self.__delete_point_async(R_FEED, pid) def delete_control(self, pid): """ Delete a control, identified by its local id. Raises: IOTException: Infrastructure problem detected LinkException: Communications problem between you and the infrastructure Args: pid (string): Local identifier of your control you want to delete """ logger.info("delete_control(pid=\"%s\") [lid=%s]", pid, self.__lid) return self.__delete_point(R_CONTROL, pid) def delete_control_async(self, pid): logger.info("delete_control_async(pid=\"%s\") [lid=%s]", pid, self.__lid) return self.__delete_point_async(R_CONTROL, pid) def __get_sub(self, foc, gpid): # global if isinstance(gpid, string_types): gpid = uuid_to_hex(gpid) # not local elif not (isinstance(gpid, Sequence) and len(gpid) == 2): raise ValueError('gpid must be string or two-element tuple') try: # Not using ThreadSafeDict lock since pop() is atomic operation sub = self.__new_subs.pop((foc, gpid)) except KeyError as ex: raise_from( KeyError('Remote%s subscription %s not know as new' % (foc_to_str(foc).capitalize(), gpid)), ex) if sub is None: raise ValueError( 'Either subscription not complete yet or point %s is of opposite type. (In which case' ' call %s instead' % (gpid, 'attach()' if foc == R_FEED else 'follow()')) return sub def get_remote_feed(self, gpid): """ Retrieve `RemoteFeed` instance for a follow. This only applies to asynchronous follow requests and the new `RemoteFeed` instance can only be retrieved once. Note: Destructive Read. Once you've called get_remote_feed once, any further calls will raise a `KeyError` Raises: KeyError: The follow-subscription has not been newly created (or has already been retrieved by a previous call ValueError: The followed Point is actually a control instead of a feed, or if the subscription has not completed yet. """ return self.__get_sub(R_FEED, gpid) def get_remote_control(self, gpid): """ Retrieve `RemoteControl` instance for an attach. This only applies to asynchronous attach requests and the new `RemoteControl` instance can only be retrieved once. Note: Destructive Read. Once you've called get_remote_control once, any further calls will raise a `KeyError` Raises: KeyError: If the attach-subscription has not been newly created (or has already been retrieved by a previous call) ValueError: If the followed Point is actually a feed instead of a control, or if the subscription has not completed yet. """ return self.__get_sub(R_CONTROL, gpid) @contextmanager def __sub_add_reference(self, key): """ Used by __sub_make_request to save reference for pending sub request """ new_subs = self.__new_subs with new_subs: # don't allow multiple subscription requests to overwrite internal reference if key in new_subs: raise ValueError('subscription for given args pending: %s' % str(key)) new_subs[key] = None try: yield except: # don't preserve reference if request creation failed with new_subs: new_subs.pop(key, None) raise def __sub_del_reference(self, req, key): """ Blindly clear reference to pending subscription on failure. """ if not req.success: try: self.__new_subs.pop(key) except KeyError: logger.warning('No sub ref %s', key) def __sub_make_request(self, foc, gpid, callback): """ Make right subscription request depending on whether local or global - used by __sub* """ # global if isinstance(gpid, string_types): gpid = uuid_to_hex(gpid) ref = (foc, gpid) with self.__sub_add_reference(ref): req = self._client._request_sub_create(self.__lid, foc, gpid, callback=callback) # local elif isinstance(gpid, Sequence) and len(gpid) == 2: ref = (foc, tuple(gpid)) with self.__sub_add_reference(ref): req = self._client._request_sub_create_local(self.__lid, foc, *gpid, callback=callback) else: raise ValueError('gpid must be string or two-element tuple') req._run_on_completion(self.__sub_del_reference, ref) return req def __sub(self, foc, gpid, callback=None): evt = self.__sub_async(foc, gpid, callback=callback) self._client._wait_and_except_if_failed(evt) try: return self.__get_sub(foc, gpid) except KeyError as ex: raise raise_from( IOTClientError( 'Subscription for %s (from %s) not in cache (post-create)' % gpid, self.__lid), ex) def __sub_async(self, foc, gpid, callback=None): logger.info("__sub(foc=%s, gpid=\"%s\", callback=%s) [lid=%s]", foc_to_str(foc), gpid, callback, self.__lid) return self.__sub_make_request(foc, gpid, callback) def follow(self, gpid, callback=None, callback_parsed=None): """ Create a subscription (i.e. follow) a Feed/Point with a global point id (gpid) and a feed data callback Returns: A new :doc:`IoticAgent.IOT.RemotePoint` RemoteFeed object or the existing one if the subscription already exists Or: Raises: IOTException: Infrastructure problem detected LinkException: Communications problem between you and the infrastructure Args: gpid (uuid): Global id of the Point you want to follow **OR** gpid (lid, pid): Tuple of `(thing_localid, point_localid)` for local subscription callback (function reference, optional): Callback function to invoke on receipt of feed data. callback_parsed (function reference, optional): Callback function to invoke on receipt of feed data. This is equivalent to `callback` except the dict includes the `parsed` key which holds the set of values in a :doc:`IoticAgent.IOT.Point` PointDataObject instance. If both `callback_parsed` and `callback` have been specified, the former takes precedence and `callback` is only called if the point data could not be parsed according to its current value description. Note: The callback receives a single dict argument, with keys of: :: 'data' # (decoded or raw bytes) 'mime' # (None, unless payload was not decoded and has a mime type) 'pid' # (the global id of the feed from which the data originates) 'time' # (datetime representing UTC timestamp of share) Note: `callback_parsed` can only be used if `auto_encode_decode` is enabled for the client instance. """ if callback_parsed: callback = self._client._get_parsed_feed_callback( callback_parsed, callback) return self.__sub(R_FEED, gpid, callback=callback) def follow_async(self, gpid, callback=None, callback_parsed=None): if callback_parsed: callback = self._client._get_parsed_feed_callback( callback_parsed, callback) return self.__sub_async(R_FEED, gpid, callback=callback) def attach(self, gpid): """ Create a subscription (i.e. attach) to a Control-Point with a global point id (gpid) and a feed data callback Returns: A new RemoteControl object from the :doc:`IoticAgent.IOT.RemotePoint` or the existing one if the subscription already exists Raises: IOTException: Infrastructure problem detected LinkException: Communications problem between you and the infrastructure Args: gpid (uuid): Global id of the Point to which you want to attach **OR** gpid (lid, pid): Tuple of `(thing_localid, point_localid)` for local subscription """ return self.__sub(R_CONTROL, gpid) def attach_async(self, gpid): return self.__sub_async(R_CONTROL, gpid) def __sub_delete(self, subid): if isinstance(subid, (RemoteFeed, RemoteControl)): subid = subid.subid evt = self._client._request_sub_delete(subid) self._client._wait_and_except_if_failed(evt) def unfollow(self, subid): """ Remove a subscription of a Feed with a global subscription id (gpid) Raises: IOTException: Infrastructure problem detected LinkException: Communications problem between you and the infrastructure Args: subid (uuid): Global id of the subscription you want to delete **OR** subid (object): The instance of a RemoteFeed object corresponding to the feed you want to cease following. """ return self.__sub_delete(subid) def unattach(self, subid): """ Remove a subscription of a control with a global subscription id (gpid) Raises: IOTException: Infrastructure problem detected LinkException: Communications problem between you and the infrastructure Args: subid (uuid): Global id of the subscription you want to delete **OR** subid (object): The instance of a RemoteControl object corresponding to the control you want to cease being able to actuate. """ return self.__sub_delete(subid) def list_connections(self, limit=500, offset=0): """ List Points to which this Things is subscribed. I.e. list all the Points this Thing is following and controls it's attached to Returns: Subscription list e.g. :: { "<Subscription GUID 1>": { "id": "<Control GUID>", "entityId": "<Control's Thing GUID>", "type": 3 # R_CONTROL from IoticAgent.Core.Const }, "<Subscription GUID 2>": { "id": "<Feed GUID>", "entityId": "<Feed's Thing GUID>", "type": 2 # R_FEED from IoticAgent.Core.Const } Raises: IOTException: Infrastructure problem detected LinkException: Communications problem between you and the infrastructure Note: For Things following a Point see :doc:`IoticAgent.IOT.Point` list_followers. """ evt = self._client._request_sub_list(self.__lid, limit=limit, offset=offset) self._client._wait_and_except_if_failed(evt) return evt.payload['subs'] def _cb_created(self, payload, duplicated): """ Indirect callback (via Client) for point & subscription creation responses """ if payload[P_RESOURCE] in _POINT_TYPE_TO_CLASS: store = self.__new_feeds if payload[ P_RESOURCE] == R_FEED else self.__new_controls cls = _POINT_TYPE_TO_CLASS[payload[P_RESOURCE]] with store: store[payload[P_LID]] = cls(self._client, payload[P_ENTITY_LID], payload[P_LID], payload[P_ID]) logger.debug('Added %s: %s (for %s)', foc_to_str(payload[P_RESOURCE]), payload[P_LID], payload[P_ENTITY_LID]) elif payload[P_RESOURCE] == R_SUB: # local if P_POINT_ENTITY_LID in payload: key = (payload[P_POINT_TYPE], (payload[P_POINT_ENTITY_LID], payload[P_POINT_LID])) # global else: key = (payload[P_POINT_TYPE], payload[P_POINT_ID]) new_subs = self.__new_subs with new_subs: if key in new_subs: cls = RemoteFeed if payload[ P_POINT_TYPE] == R_FEED else RemoteControl new_subs[key] = cls(self._client, payload[P_ID], payload[P_POINT_ID], payload[P_ENTITY_LID]) else: logger.warning( 'Ignoring subscription creation for unexpected %s: %s', foc_to_str(payload[P_POINT_TYPE]), key[1]) else: logger.error('Resource creation of type %d unhandled', payload[P_RESOURCE]) def _cb_reassigned(self, payload): self.__epId = payload[P_EPID] logger.info('Thing %s reassigned to agent %s', self.__lid, self.__epId)
def __init__(self, config=None, db=None): """ Creates an IOT.Client instance which provides access to Iotic Space `config` (optional): The name of the config file containing the connection parameters. Defaults to the name of the script +".ini", e.g. `config="my_script.ini"`. Alternatively an existing [Config](Config.m.html#IoticAgent.IOT.Config.Config) object can be specified. `db` (optional): The name of the database file to be used for storing key-value pairs by the agent. Defaults to the name of the script +".db", e.g. `db="my_script.db"` """ self.__core_version_check() logger.info('IOT version: %s', __version__) # if isinstance(config, string_types) or config is None: self.__config = Config(config) elif isinstance(config, Config): self.__config = config else: raise ValueError('config should be string or Config instance') # if any(self.__config.get('agent', item) is None for item in ('host', 'epid', 'passwd', 'token')): raise ValueError('Minimum configuration for IoticAgent is host, epid, passwd and token\n' 'Create file "%s" with contents\n[agent]\nhost = w\nepid = x\npasswd = y\ntoken = z' % self.__config._file_loc()) self.__sync_timeout = validate_nonnegative_int(self.__config.get('iot', 'sync_request_timeout'), 'iot.sync_request_timeout', allow_zero=False) self.__config.setup_logging() # try: self.__client = Core_Client(host=self.__config.get('agent', 'host'), vhost=self.__config.get('agent', 'vhost'), epId=self.__config.get('agent', 'epid'), lang=self.__config.get('agent', 'lang'), passwd=self.__config.get('agent', 'passwd'), token=self.__config.get('agent', 'token'), prefix=self.__config.get('agent', 'prefix'), sslca=self.__config.get('agent', 'sslca'), network_retry_timeout=self.__config.get('core', 'network_retry_timeout'), socket_timeout=self.__config.get('core', 'socket_timeout'), auto_encode_decode=bool_from(self.__config.get('core', 'auto_encode_decode')), send_queue_size=self.__config.get('core', 'queue_size'), throttle_conf=self.__config.get('core', 'throttle')) except ValueError as ex: raise_from(ValueError('Configuration error'), ex) # # Callbacks for updating own resource cache (used internally only) self.__client.register_callback_created(self.__cb_created) self.__client.register_callback_duplicate(self.__cb_duplicated) self.__client.register_callback_reassigned(self.__cb_reassigned) self.__client.register_callback_deleted(self.__cb_deleted) # # Setup catchall for unsolicited feeddata and controlreq and write data to the key/value DB self.__db = None if bool_from(self.__config.get('iot', 'db_last_value')): self.__db = DB(fn=db) self.__client.register_callback_feeddata(self.__cb_catchall_feeddata) self.__client.register_callback_controlreq(self.__cb_catchall_controlreq) # # Keeps track of newly created things (the requests for which originated from this agent) self.__new_things = ThreadSafeDict() # Allows client to forward e.g. Point creation callbacks to the relevant Thing instance. This contains the most # recent instance of any single Thing (since creation requests can be performed more than once). self.__private_things = ThreadSafeDict()
class Client(object): # pylint: disable=too-many-public-methods # Core version targeted by IOT client __core_version = '0.3.1' def __init__(self, config=None, db=None): """ Creates an IOT.Client instance which provides access to Iotic Space `config` (optional): The name of the config file containing the connection parameters. Defaults to the name of the script +".ini", e.g. `config="my_script.ini"`. Alternatively an existing [Config](Config.m.html#IoticAgent.IOT.Config.Config) object can be specified. `db` (optional): The name of the database file to be used for storing key-value pairs by the agent. Defaults to the name of the script +".db", e.g. `db="my_script.db"` """ self.__core_version_check() logger.info('IOT version: %s', __version__) # if isinstance(config, string_types) or config is None: self.__config = Config(config) elif isinstance(config, Config): self.__config = config else: raise ValueError('config should be string or Config instance') # if any(self.__config.get('agent', item) is None for item in ('host', 'epid', 'passwd', 'token')): raise ValueError('Minimum configuration for IoticAgent is host, epid, passwd and token\n' 'Create file "%s" with contents\n[agent]\nhost = w\nepid = x\npasswd = y\ntoken = z' % self.__config._file_loc()) self.__sync_timeout = validate_nonnegative_int(self.__config.get('iot', 'sync_request_timeout'), 'iot.sync_request_timeout', allow_zero=False) self.__config.setup_logging() # try: self.__client = Core_Client(host=self.__config.get('agent', 'host'), vhost=self.__config.get('agent', 'vhost'), epId=self.__config.get('agent', 'epid'), lang=self.__config.get('agent', 'lang'), passwd=self.__config.get('agent', 'passwd'), token=self.__config.get('agent', 'token'), prefix=self.__config.get('agent', 'prefix'), sslca=self.__config.get('agent', 'sslca'), network_retry_timeout=self.__config.get('core', 'network_retry_timeout'), socket_timeout=self.__config.get('core', 'socket_timeout'), auto_encode_decode=bool_from(self.__config.get('core', 'auto_encode_decode')), send_queue_size=self.__config.get('core', 'queue_size'), throttle_conf=self.__config.get('core', 'throttle')) except ValueError as ex: raise_from(ValueError('Configuration error'), ex) # # Callbacks for updating own resource cache (used internally only) self.__client.register_callback_created(self.__cb_created) self.__client.register_callback_duplicate(self.__cb_duplicated) self.__client.register_callback_reassigned(self.__cb_reassigned) self.__client.register_callback_deleted(self.__cb_deleted) # # Setup catchall for unsolicited feeddata and controlreq and write data to the key/value DB self.__db = None if bool_from(self.__config.get('iot', 'db_last_value')): self.__db = DB(fn=db) self.__client.register_callback_feeddata(self.__cb_catchall_feeddata) self.__client.register_callback_controlreq(self.__cb_catchall_controlreq) # # Keeps track of newly created things (the requests for which originated from this agent) self.__new_things = ThreadSafeDict() # Allows client to forward e.g. Point creation callbacks to the relevant Thing instance. This contains the most # recent instance of any single Thing (since creation requests can be performed more than once). self.__private_things = ThreadSafeDict() @property def agent_id(self): """Agent id (aka epId) in use for this client instance""" return self.__client.epId @property def default_lang(self): """Language in use when not explicitly specified (in meta related requests). Will be set to container default if was not set in configuration. Before client has started this might be None.""" return self.__client.default_lang def start(self): """Open a connection to Iotic Space. `start()` is called by `__enter__` which allows the python `with` syntax to be used. `Example 0` - Calling start() implicitly using with. minimal example with no exception handling #!python with IOT.Client(config="my_script.ini") as client: client.create_thing("my_thing") `Example 1` - Calling start() implicitly using with. This handles the finally for you. `Recommended` #!python try: with IOT.Client(config="my_script.ini") as client: try: client.create_thing("my_thing") except IOTException as exc: # handle exception except Exception as exc: # not possible to connect print(exc) import sys sys.exit(1) `Example 2` - Calling start() explicitly (no with) Be careful, you have to put a finally in your try blocks otherwise your client might remain connected. #!python try: client = IOT.Client(config="my_script.ini") # wire up callbacks here client.start() except Exception as exc: print(exc) import sys sys.exit(1) try: client.create_thing("my_thing") except IOTException as exc: # handle individual exception finally: client.stop() Returns This Client instance Raises Exception """ if not self.__client.is_alive(): self.__client.start() return self def __enter__(self): return self.start() def stop(self): """Close the connection to Iotic Space. `stop()` is called by `__exit()__` allowing the python `with` syntax to be used. See [start()](#IoticAgent.IOT.Client.Client.start) """ if self.__client.is_alive(): self.__client.stop() def close(self): """`DEPRECATED`. Please note IOT.Client now requires a .start call. close becomes stop. """ warnings.warn('close() has been deprecated, use stop() instead', DeprecationWarning) self.stop() def __exit__(self, exc_type, exc_value, traceback): return self.stop() def __del__(self): try: self.stop() # Don't want on ignored exceptions except: pass @property def sync_timeout(self): """Value of iot.sync_request_timeout configuration option. Used by all synchronous requests to limit total request wait time.""" return self.__sync_timeout @classmethod def __core_version_check(cls): core = version_string_to_tuple(Core_Version) expected = version_string_to_tuple(cls.__core_version) if core[0] != expected[0]: raise RuntimeError('Core client dependency major version difference: %s (%s expected)' % (Core_Version, cls.__core_version)) elif core[1] < expected[1]: raise RuntimeError('Core client minor version old: %s (%s known)' % (Core_Version, cls.__core_version)) elif core[1] > expected[1]: logger.warning('Core client minor version difference: %s (%s known)', Core_Version, cls.__core_version) elif core[2] > expected[2]: logger.info('Core client patch level change: %s (%s known)', Core_Version, cls.__core_version) else: logger.info('Core version: %s', Core_Version) def _notify_thing_lid_change(self, from_lid, to_lid): """Used by Thing instances to indicate that a rename operation has happened""" try: with self.__private_things: self.__private_things[to_lid] = self.__private_things.pop(from_lid) except KeyError: logger.warning('Thing %s renamed (to %s), but not in private lookup table', from_lid, to_lid) else: # renaming could happen before get_thing is called on the original try: with self.__new_things: self.__new_things[to_lid] = self.__new_things.pop(from_lid) except KeyError: pass def is_connected(self): """ Returns client's alive state """ return self.__client.is_alive() def register_catchall_feeddata(self, callback): """ Registers a callback that is called for all feeddata your Thing receives `Example` #!python def feeddata_callback(data): print(data) ... client.register_catchall_feeddata(feeddata_callback) `callback` (required) the function name that you want to be called on receipt of new feed data More details on the contents of the `data` dictionary for feeds see: [follow()](./Thing.m.html#IoticAgent.IOT.Thing.Thing.follow) """ return self.__client.register_callback_feeddata(callback) def register_catchall_controlreq(self, callback): """ Registers a callback that is called for all control requests received by your Thing `Example` #!python def controlreq_callback(data): print(data) ... client.register_catchall_controlreq(controlreq_callback) `callback` (required) the function name that you want to be called on receipt of a new control request More details on the contents of the `data` dictionary for controls see: [create_control()](./Thing.m.html#IoticAgent.IOT.Thing.Thing.create_control) """ return self.__client.register_callback_controlreq(callback) def register_callback_subscription(self, callback): """ Register a callback for subscription count change notification. This gets called whenever something *else* subscribes to your thing. `Note` it is not called when you subscribe to something else. The payload passed to your callback is an OrderedDict with the following keys #!python r : R_FEED or R_CONTROL # the type of the point lid : <name> # the local name of your *Point* entityLid : <name> # the local name of your *Thing* subCount : <count> # the total number of remote Things # that subscribe to your point `Example` #!python def subscription_callback(args): print(args) ... client.register_callback_subscription(subscription_callback) This would print out something like the following #!python OrderedDict([('r', 2), ('lid', 'My_First_Point'), ('entityLid', 'My_First_Thing'), ('subCount', 13)]) """ return self.__client.register_callback_subscription(callback) def simulate_feeddata(self, feedid, data=None, mime=None): """Simulate the last feeddata received for given feedid Calls the registered callback for the feed with the last recieved feed data. Allows you to test your code without having to wait for the remote thing to share again. Raises KeyError - if there is no data with which to simulate. I.e. you haven't received any and haven't used the data= and mime= parameters Raises RuntimeError - if the key-value store "database" is disabled `feedid` (required) (string) local id of your Feed `data` (optional) (as applicable) The data you want to use to simulate the arrival of remote feed data `mime` (optional) (string) The mime type of your data. See: [share()](./Point.m.html#IoticAgent.IOT.Point.Point.share) """ if data is None: if self.__db is None: raise RuntimeError("simulate_feeddata disabled with [iot] db_last_value = 0") # can raise KeyError if not available data, mime = self.__db.kv_get(feedid) self.__client.simulate_feeddata(feedid, data, mime) # # todo: simulate_controlreq -- needs last data and Point instance # def get_last_feeddata(self, feedid): """Get the value of the last feed data from a remote thing, if any has been received Returns last data for feedid and mime (as tuple), or tuple of (None, None) if not found Raises KeyError - if there is no data to get. Probably because you haven't received any and haven't called [simulate_feeddata()](#IoticAgent.IOT.Client.Client.simulate_feeddata) with the data= and mime= parameters set Raises RuntimeError - if the key-value store "database" is disabled `feedid` (required) (string) local id of your Feed """ if self.__db is None: raise RuntimeError("get_last_feeddata disabled with [iot] db_last_value = 0") try: return self.__db.kv_get(feedid) # data, mime except KeyError: return None, None def confirm_tell(self, data, success): """Confirm that you've done as you were told. Call this from your control callback to confirm action. Used when you are advertising a control and you want to tell the remote requestor that you have done what they asked you to. `Example:` this is a minimal example to show the idea. Note - no Exception handling and ugly use of globals #!python client = None def controlreq_cb(args): global client # the client object you connected with # perform your action with the data they sent success = do_control_action(args['data']) if args['confirm']: # you've been asked to confirm client.confirm_tell(args, success) # else, if you do not confirm_tell() this causes a timeout at the requestor's end. client = IOT.Client(config='test.ini') thing = client.create_thing('test321') control = thing.create_control('control', controlreq_cb) Raises [IOTException](./Exceptions.m.html#IoticAgent.IOT.Exceptions.IOTException) containing the error if the infrastructure detects a problem Raises [LinkException](../Core/AmqpLink.m.html#IoticAgent.Core.AmqpLink.LinkException) if there is a communications problem between you and the infrastructure `data` (mandatory) (dictionary) The `"args"` dictionary that your callback was called with `success` (mandatory) (boolean) Whether or not the action you have been asked to do has been sucessful. More details on the contents of the `data` dictionary for controls see: [create_control()](./Thing.m.html#IoticAgent.IOT.Thing.Thing.create_control) """ logger.info("confirm_tell(success=%s) [lid=\"%s\",pid=\"%s\"]", success, data[P_ENTITY_LID], data[P_LID]) evt = self._request_point_confirm_tell(R_CONTROL, data[P_ENTITY_LID], data[P_LID], success, data['requestId']) evt.wait(self.__sync_timeout) self._except_if_failed(evt) def save_config(self): """Save the config, update the seqnum & default language """ self.__config.set('agent', 'seqnum', self.__client.get_seqnum()) self.__config.set('agent', 'lang', self.__client.default_lang) self.__config.save() @staticmethod def _except_if_failed(event): """Raises an IOTException from the given event if it was not successful. Assumes timeout success flag on event has not been set yet.""" if event.success is None: raise IOTSyncTimeout('Requested timed out') if not event.success: msg = "Request failed, unknown error" if isinstance(event.payload, Mapping): if P_MESSAGE in event.payload: msg = event.payload[P_MESSAGE] if P_CODE in event.payload: code = event.payload[P_CODE] if code == E_FAILED_CODE_ACCESSDENIED: raise IOTAccessDenied(msg) if code == E_FAILED_CODE_INTERNALERROR: raise IOTInternalError(msg) if code in (E_FAILED_CODE_MALFORMED, E_FAILED_CODE_NOTALLOWED): raise IOTMalformed(msg) if code == E_FAILED_CODE_UNKNOWN: raise IOTUnknown(msg) raise IOTException(msg) def list(self, all_my_agents=False, limit=500, offset=0): """List `all` the things created by this client on this or all your agents Returns QAPI list function payload Raises [LinkException](../Core/AmqpLink.m.html#IoticAgent.Core.AmqpLink.LinkException) if there is a communications problem between you and the infrastructure `all_my_agents` (optional) (boolean) If `False` limit search to just this agent, if `True` return list of things belonging to all agents you own. `limit` (optional) (integer) Return this many Point details `offset` (optional) (integer) Return Point details starting at this offset """ logger.info("list(all_my_agents=%s, limit=%s, offset=%s)", all_my_agents, limit, offset) if all_my_agents: evt = self._request_entity_list_all(limit=limit, offset=offset) else: evt = self._request_entity_list(limit=limit, offset=offset) evt.wait(self.__sync_timeout) self._except_if_failed(evt) return evt.payload['entities'] def get_thing(self, lid): """Get the details of a newly created Thing. This only applies to asynchronous creation of Things and the new Thing instance can only be retrieved once. Returns a [Thing](Thing.m.html#IoticAgent.IOT.Thing.Thing) object, which corresponds to the Thing with the given local id (nickname) Raises `KeyError` if the Thing has not been newly created (or has already been retrieved by a previous call) `lid` (required) (string) local identifier of your Thing. """ with self.__new_things: try: return self.__new_things.pop(lid) except KeyError as ex: raise_from(KeyError('Thing %s not know as new' % lid), ex) def create_thing(self, lid): """Create a new Thing with a local id (lid). Returns a [Thing](Thing.m.html#IoticAgent.IOT.Thing.Thing) object if successful or if the Thing already exists Raises [IOTException](./Exceptions.m.html#IoticAgent.IOT.Exceptions.IOTException) containing the error if the infrastructure detects a problem Raises [LinkException](../Core/AmqpLink.m.html#IoticAgent.Core.AmqpLink.LinkException) if there is a communications problem between you and the infrastructure `lid` (required) (string) local identifier of your Thing. The local id is your name or nickname for the thing. It's "local" in that it's only available to you on this container, not searchable and not visible to others. """ logger.info("create_thing(lid=\"%s\")", lid) evt = self._request_entity_create(lid) evt.wait(self.__sync_timeout) self._except_if_failed(evt) try: with self.__new_things: return self.__new_things.pop(lid) except KeyError as ex: raise raise_from(IOTClientError('Thing %s not in cache (post-create)' % lid), ex) def create_thing_async(self, lid): logger.info("create_thing_async(lid=\"%s\")", lid) return self._request_entity_create(lid) def delete_thing(self, lid): """Delete a Thing Raises [IOTException](./Exceptions.m.html#IoticAgent.IOT.Exceptions.IOTException) containing the error if the infrastructure detects a problem Raises [LinkException](../Core/AmqpLink.m.html#IoticAgent.Core.AmqpLink.LinkException) if there is a communications problem between you and the infrastructure `lid` (required) (string) local identifier of the Thing you want to delete """ logger.info("delete_thing(lid=\"%s\")", lid) evt = self._request_entity_delete(lid) evt.wait(self.__sync_timeout) self._except_if_failed(evt) def delete_thing_async(self, lid): logger.info("delete_thing_async(lid=\"%s\")", lid) return self._request_entity_delete(lid) def search(self, text=None, lang=None, location=None, unit=None, limit=50, offset=0, reduced=False): """Search the Iotic Space for public Things with metadata matching the search parameters: text, (lang, location, unit, limit, offset)=optional. Note that only things which have at least one point defined can be found. Returns dict of results as below (first with reduced=False, second with reduced=True)- OR - #!python # reduced=False returns dict similar to below { "2b2d8b068e404861b19f9e060877e002": { "long": "-1.74803", "matches": 3.500, "lat": "52.4539", "label": "Weather Station #2", "points": { "a300cc90147f4e2990195639de0af201": { "matches": 3.000, "label": "Feed 201", "type": "Feed" }, "a300cc90147f4e2990195639de0af202": { "matches": 1.500, "label": "Feed 202", "type": "Feed" } } }, "76a3b24b02d34f20b675257624b0e001": { "long": "0.716356", "matches": 2.000, "lat": "52.244384", "label": "Weather Station #1", "points": { "fb1a4a4dbb2642ab9f836892da93f101": { "matches": 1.000, "label": "My weather feed", "type": "Feed" }, "fb1a4a4dbb2642ab9f836892da93c102": { "matches": 1.000, "label": None, "type": "Control" } } } } # reduced=True returns dict similar to below { "2b2d8b068e404861b19f9e060877e002": { "a300cc90147f4e2990195639de0af201": "Feed", "a300cc90147f4e2990195639de0af202": "Feed" }, "76a3b24b02d34f20b675257624b0e001": { "fb1a4a4dbb2642ab9f836892da93f101": "Feed", "fb1a4a4dbb2642ab9f836892da93f102": "Control" } } Raises [IOTException](./Exceptions.m.html#IoticAgent.IOT.Exceptions.IOTException) containing the error if the infrastructure detects a problem Raises [LinkException](../Core/AmqpLink.m.html#IoticAgent.Core.AmqpLink.LinkException) if there is a communications problem between you and the infrastructure `text` (required) (string) The text to search for. Label and description will be searched for both Thing and Point and each word will be used as a tag search too. Text search is case-insensitive. `lang` (optional) (string) The two-character ISO 639-1 language code to search in, e.g. "en" "fr" Language is used to limit search to only labels and descriptions in that language. You will only get labels `in that language` back from search and then only if there are any in that language `location` (optional) (dictionary) Latitude, longitude and radius to search within. All values are float, Radius is in kilometers (km). E.g. `{"lat"=1.2345, "lon"=54.321, "radius"=6.789}` `unit` (optional) (string) Valid URL of a unit in an ontology. Or use a constant from the [units](../Units.m.html#IoticAgent.Units) class - such as [METRE](../Units.m.html#IoticAgent.Units.METRE) `limit` (optional) (integer) Return this many search results `offset` (optional) (integer) Return results starting at this offset - good for paging. `reduced` (optional) (boolean) If `true`, Return the reduced results just containing points and their type """ logger.info("search(text=\"%s\", lang=\"%s\", location=\"%s\", unit=\"%s\", limit=%s, offset=%s, reduced=%s)", text, lang, location, unit, limit, offset, reduced) evt = self._request_search(text, lang, location, unit, limit, offset, 'reduced' if reduced else 'full') evt.wait(self.__sync_timeout) self._except_if_failed(evt) return evt.payload['result'] # pylint: disable=unsubscriptable-object def search_reduced(self, text=None, lang=None, location=None, unit=None, limit=100, offset=0): """Shorthand for [search()](#IoticAgent.IOT.Client.Client.search) with `reduced=True`""" return self.search(text, lang, location, unit, limit, offset, reduced=True) def search_located(self, text=None, lang=None, location=None, unit=None, limit=100, offset=0): """See [search()](#IoticAgent.IOT.Client.Client.search) for general documentation. Provides a thing-only result set comprised only of things which have a location set, e.g.: #!python { # Keyed by thing id '2b2d8b068e404861b19f9e060877e002': # location (g, lat & long), label (l, optional) {'g': (52.4539, -1.74803), 'l': 'Weather Station #2'}, '76a3b24b02d34f20b675257624b0e001': {'g': (52.244384, 0.716356), 'l': None}, '76a3b24b02d34f20b675257624b0e004': {'g': (52.245384, 0.717356), 'l': 'Gasometer'}, '76a3b24b02d34f20b675257624b0e005': {'g': (52.245384, 0.717356), 'l': 'Zepellin'} } """ logger.info("search_located(text=\"%s\", lang=\"%s\", location=\"%s\", unit=\"%s\", limit=%s, offset=%s)", text, lang, location, unit, limit, offset) evt = self._request_search(text, lang, location, unit, limit, offset, 'located') evt.wait(self.__sync_timeout) self._except_if_failed(evt) return evt.payload['result'] # pylint: disable=unsubscriptable-object # used by describe() __guid_resources = (Thing, Point, RemoteFeed, RemoteControl) def describe(self, guid_or_thing, lang=None): """Describe returns the public description of a Thing Returns the description dict (see below for Thing example) if Thing or Point is public, otherwise `None` #!python { "type": "Entity", "meta": { "long": 0.716356, "lat": 52.244384, "label": "Weather Station #1", "points": [ { "type": "Control", "label": "Control 101", "guid": "fb1a4a4dbb2642ab9f836892da93c101" }, { "type": "Feed", "label": "My weather feed", "guid": "fb1a4a4dbb2642ab9f836892da93f101" } ], "comment": "A lovely weather station...", "tags": [ "blue", "garden" ] } } Raises [IOTException](./Exceptions.m.html#IoticAgent.IOT.Exceptions.IOTException) containing the error if the infrastructure detects a problem Raises [LinkException](../Core/AmqpLink.m.html#IoticAgent.Core.AmqpLink.LinkException) if there is a communications problem between you and the infrastructure `guid_or_thing` (mandatory) (string or object). If a `string`, it should contain the globally unique id of the resource you want to describe in 8-4-4-4-12 format. If an `object`, it should be an instance of Thing, Point, RemoteFeed or RemoteControl. The system will return you the description of that object. `lang` (optional) (string) The two-character ISO 639-1 language code for which labels, comments and tags will be returned. This does not affect Values (i.e. when describing a Point). """ if isinstance(guid_or_thing, self.__guid_resources): guid = guid_or_thing.guid elif isinstance(guid_or_thing, string_types): guid = uuid_to_hex(guid_or_thing) else: raise ValueError("describe requires guid string or Thing, Point, RemoteFeed or RemoteControl instance") logger.info('describe() [guid="%s"]', guid) evt = self._request_describe(guid, lang) evt.wait(self.__sync_timeout) self._except_if_failed(evt) return evt.payload['result'] # pylint: disable=unsubscriptable-object def __cb_created(self, msg, duplicated=False): # Only consider solicitied creation events since there is no cache. This also applies to things reassigned to # this agent. if msg[M_CLIENTREF] is not None: payload = msg[M_PAYLOAD] if payload[P_RESOURCE] == R_ENTITY: lid = payload[P_LID] if payload[P_EPID] != self.__client.epId: logger.warning('Created thing %s assigned to different agent: %s', lid, payload[P_EPID]) thing = Thing(self, lid, payload[P_ID], payload[P_EPID]) with self.__new_things: self.__new_things[lid] = thing # second (permanent) reference kept so can forward to thing for e.g. point creation callbacks with self.__private_things: self.__private_things[lid] = thing logger.debug('Added %sthing: %s (%s)', 'existing ' if duplicated else '', lid, payload[P_ID]) elif payload[P_RESOURCE] in _POINT_TYPES or payload[P_RESOURCE] == R_SUB: with self.__private_things: thing = self.__private_things.get(payload[P_ENTITY_LID], None) if thing: thing._cb_created(payload, duplicated=duplicated) else: logger.warning('Thing %s unknown internally, ignoring creation of point/sub', payload[P_ENTITY_LID]) else: logger.error('Resource creation of type %d unhandled', payload[P_RESOURCE]) else: logger.debug('Ignoring unsolicited creation request of type %d', payload[P_RESOURCE]) def __cb_duplicated(self, msg): self.__cb_created(msg, duplicated=True) def __cb_reassigned(self, msg): payload = msg[M_PAYLOAD] if payload[P_RESOURCE] == R_ENTITY: with self.__private_things: thing = self.__private_things.get(payload[P_LID], None) if thing: thing._cb_reassigned(payload) else: logger.warning('Thing %s unknown internally, ignoring reassignment', payload[P_LID]) else: logger.error('Resource reassignment of type %d unhandled', payload[P_RESOURCE]) def __cb_deleted(self, msg): # only consider solicitied deletion events if msg[M_CLIENTREF] is not None: payload = msg[M_PAYLOAD] if payload[P_RESOURCE] == R_ENTITY: try: with self.__private_things: self.__private_things.pop(payload[P_LID]) except KeyError: logger.warning('Deleted thing %s unknown internally', payload[P_LID]) else: logger.debug('Deleted thing: %s', payload[P_LID]) # currently no functionality benefits from these elif payload[P_RESOURCE] in (R_FEED, R_CONTROL, R_SUB): pass else: logger.error('Resource deletetion of type %d unhandled', payload[P_RESOURCE]) else: logger.debug('Ignoring unsolicited deletion request of type %d', msg[M_PAYLOAD][P_RESOURCE]) def __cb_catchall_feeddata(self, data): try: # todo - confusing to store feed & control data in same dictionary? self.__db.kv_set(data['pid'], (data[P_DATA], data[P_MIME])) except (IOError, OSError): logger.warning('Failed to write feed data for %s to db', data['pid'], exc_info=DEBUG_ENABLED) def __cb_catchall_controlreq(self, data): try: # todo - confusing to store feed & control data in same dictionary? # (although shouldn't clash since here includes spae and pid (above for feeds) should not self.__db.kv_set('%s %s' % (data[P_ENTITY_LID], data[P_LID]), (data[P_DATA], data[P_MIME])) except (IOError, OSError): logger.warning('Failed to write control request data for lid=%s pid=%s to db', data[P_ENTITY_LID], data[P_LID], exc_info=DEBUG_ENABLED) # Wrap Core.Client functions so IOT contains everything. # These are protected functions used by Client, Thing, Point etc. def _request_point_list(self, foc, lid, limit, offset): return self.__client.request_point_list(foc, lid, limit, offset) def _request_point_list_detailed(self, foc, lid, pid): return self.__client.request_point_list_detailed(foc, lid, pid) def _request_entity_list(self, limit=0, offset=500): return self.__client.request_entity_list(limit=limit, offset=offset) def _request_entity_list_all(self, limit=0, offset=500): return self.__client.request_entity_list_all(limit=limit, offset=offset) def _request_entity_create(self, lid): return self.__client.request_entity_create(lid) def _request_entity_rename(self, lid, new_lid): return self.__client.request_entity_rename(lid, new_lid) def _request_entity_delete(self, lid): return self.__client.request_entity_delete(lid) def _request_entity_reassign(self, lid, new_epid): return self.__client.request_entity_reassign(lid, new_epid) def _request_entity_meta_setpublic(self, lid, public): return self.__client.request_entity_meta_setpublic(lid, public) def _request_entity_tag_create(self, lid, tags, lang, delete=False): return self.__client.request_entity_tag_create(lid, tags, lang, delete) def _request_entity_tag_delete(self, lid, tags, lang): return self._request_entity_tag_create(lid, tags, lang, delete=True) def _request_entity_tag_list(self, lid, limit, offset): return self.__client.request_entity_tag_list(lid, limit, offset) def _request_entity_meta_get(self, lid, fmt): return self.__client.request_entity_meta_get(lid, fmt) def _request_entity_meta_set(self, lid, rdf, fmt): return self.__client.request_entity_meta_set(lid, rdf, fmt) def _request_point_create(self, foc, lid, pid, control_cb=None): return self.__client.request_point_create(foc, lid, pid, control_cb) def _request_point_rename(self, foc, lid, pid, newpid): return self.__client.request_point_rename(foc, lid, pid, newpid) def _request_point_delete(self, foc, lid, pid): return self.__client.request_point_delete(foc, lid, pid) def _request_point_share(self, lid, pid, data, mime): return self.__client.request_point_share(lid, pid, data, mime) def _request_point_confirm_tell(self, foc, lid, pid, success, requestId): return self.__client.request_point_confirm_tell(foc, lid, pid, success, requestId) def _request_point_meta_get(self, foc, lid, pid, fmt): return self.__client.request_point_meta_get(foc, lid, pid, fmt) def _request_point_meta_set(self, foc, lid, pid, rdf, fmt): return self.__client.request_point_meta_set(foc, lid, pid, rdf, fmt) def _request_point_tag_create(self, foc, lid, pid, tags, lang, delete=False): return self.__client.request_point_tag_create(foc, lid, pid, tags, lang, delete) def _request_point_tag_delete(self, foc, lid, pid, tags, lang): return self._request_point_tag_create(foc, lid, pid, tags, lang, delete=True) def _request_point_tag_list(self, foc, lid, pid, limit, offset): return self.__client.request_point_tag_list(foc, lid, pid, limit, offset) def _request_point_value_create(self, lid, pid, foc, label, vtype, lang, comment, unit): return self.__client.request_point_value_create(lid, pid, foc, label, vtype, lang, comment, unit) def _request_point_value_delete(self, lid, pid, foc, label, lang): return self.__client.request_point_value_delete(lid, pid, foc, label, lang) def _request_point_value_list(self, lid, pid, foc, limit, offset): return self.__client.request_point_value_list(lid, pid, foc, limit, offset) def _request_sub_create_local(self, slid, foc, lid, pid, callback): return self.__client.request_sub_create_local(slid, foc, lid, pid, callback) def _request_sub_create(self, lid, foc, gpid, callback): return self.__client.request_sub_create(lid, foc, gpid, callback) def _request_sub_ask(self, subid, data, mime): return self.__client.request_sub_ask(subid, data, mime) def _request_sub_tell(self, subid, data, timeout, mime): return self.__client.request_sub_tell(subid, data, timeout, mime) def _request_sub_delete(self, subid): return self.__client.request_sub_delete(subid) def _request_sub_list(self, lid, limit, offset): return self.__client.request_sub_list(lid, limit, offset) def _request_search(self, text, lang, location, unit, limit, offset, type_='full'): return self.__client.request_search(text, lang, location, unit, limit, offset, type_) def _request_describe(self, guid, lang): return self.__client.request_describe(guid, lang)