Beispiel #1
0
 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()
Beispiel #2
0
 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()
Beispiel #3
0
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)
Beispiel #4
0
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)
Beispiel #5
0
    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()
Beispiel #6
0
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)