From 3602ff8e878e0251f46286e1b8244264eb40e6f6 Mon Sep 17 00:00:00 2001 From: Chris Rogers Date: Sat, 11 May 2019 08:47:33 -0400 Subject: [PATCH 1/6] Begin support for state queries --- mycroft/skills/common_iot_skill.py | 55 ++++++++++++++++++++++++------ 1 file changed, 45 insertions(+), 10 deletions(-) diff --git a/mycroft/skills/common_iot_skill.py b/mycroft/skills/common_iot_skill.py index 25c923c0f4..a96888b981 100644 --- a/mycroft/skills/common_iot_skill.py +++ b/mycroft/skills/common_iot_skill.py @@ -81,15 +81,36 @@ class Thing(Enum): class Attribute(Enum): """ This class represents 'Attributes' of 'Things'. - - This may also grow to encompass states, e.g. - 'locked' or 'unlocked'. """ BRIGHTNESS = auto() COLOR = auto() COLOR_TEMPERATURE = auto() +@unique +class State(Enum): + """ + This class represents 'States' of 'Things'. + + These are generally intended to handle binary + queries, such as "is the door locked?" or + "is the heat on?" where 'locked' and 'on' + are the state values. The special value + 'STATE' can be used for more general queries + capable of providing more detailed in formation, + for example, "what is the state of the lamp?" + could produce state information that includes + brightness or color. + """ + STATE = auto() + ON = auto() + OFF = auto() + LOCKED = auto() + UNLOCKED = auto() + OCCUPIED = auto() + UNOCCUPIED = auto() + + @unique class Action(Enum): """ @@ -106,6 +127,7 @@ class Action(Enum): INCREASE = auto() DECREASE = auto() TRIGGER = auto() + QUERY = auto() @total_ordering @@ -135,12 +157,14 @@ class IoTRequestVersion(Enum): V1 = {'action', 'thing', 'attribute', 'entity', 'scene'} V2 = V1 | {'value'} + V3 = V2 | {'state'} """ def __lt__(self, other): return self.name < other.name V1 = {'action', 'thing', 'attribute', 'entity', 'scene'} V2 = V1 | {'value'} + V3 = V2 | {'state'} class IoTRequest(): @@ -151,11 +175,13 @@ class IoTRequest(): a user's request. The information is supplied as properties on the request. At present, those properties are: - action (see the Action enum above) - thing (see the Thing enum above) + action (see the Action enum) + thing (see the Thing enum) + state (see the State enum) + attribute (see the Attribute enum) + value entity scene - value The 'action' is mandatory, and will always be not None. The other fields may be None. @@ -186,7 +212,8 @@ class IoTRequest(): attribute: Attribute = None, entity: str = None, scene: str = None, - value: int = None): + value: int = None, + state: State = None): if not thing and not entity and not scene: raise Exception("At least one of thing," @@ -198,6 +225,7 @@ class IoTRequest(): self.entity = entity self.scene = scene self.value = value + self.state = state def __repr__(self): template = ('IoTRequest(' @@ -206,7 +234,8 @@ class IoTRequest(): ' attribute={attribute},' ' entity={entity},' ' scene={scene},' - ' value={value}' + ' value={value},' + ' state={state}' ')') return template.format( action=self.action, @@ -214,11 +243,14 @@ class IoTRequest(): attribute=self.attribute, entity='"{}"'.format(self.entity) if self.entity else None, scene='"{}"'.format(self.scene) if self.scene else None, - value='"{}"'.format(self.value) if self.value is not None else None + value='"{}"'.format(self.value) if self.value is not None else None, + state=self.state ) @property def version(self): + if self.state is not None: + return IoTRequestVersion.V3 if self.value is not None: return IoTRequestVersion.V2 return IoTRequestVersion.V1 @@ -230,7 +262,8 @@ class IoTRequest(): 'attribute': self.attribute.name if self.attribute else None, 'entity': self.entity, 'scene': self.scene, - 'value': self.value + 'value': self.value, + 'state': self.state.name if self.state else None } @classmethod @@ -241,6 +274,8 @@ class IoTRequest(): data['thing'] = Thing[data['thing']] if data.get('attribute') not in (None, ''): data['attribute'] = Attribute[data['attribute']] + if data.get('state') not in (None, ''): + data['state'] = State[data['state']] return cls(**data) From 44d51d15f3ff3ecfc63a3f085842160a3fd837fd Mon Sep 17 00:00:00 2001 From: Chris Rogers Date: Tue, 28 May 2019 16:58:28 -0400 Subject: [PATCH 2/6] Add speak support --- mycroft/skills/common_iot_skill.py | 64 +++++++++++++++++++++++++++++- 1 file changed, 63 insertions(+), 1 deletion(-) diff --git a/mycroft/skills/common_iot_skill.py b/mycroft/skills/common_iot_skill.py index a96888b981..aea150a396 100644 --- a/mycroft/skills/common_iot_skill.py +++ b/mycroft/skills/common_iot_skill.py @@ -19,8 +19,9 @@ # BE WARNED THAT THE CLASSES, FUNCTIONS, ETC MAY CHANGE WITHOUT WARNING. from abc import ABC, abstractmethod +from contextlib import contextmanager from enum import Enum, unique -from functools import total_ordering +from functools import total_ordering, wraps from itertools import count from mycroft import MycroftSkill @@ -29,6 +30,7 @@ from mycroft.messagebus.message import Message ENTITY = "ENTITY" SCENE = "SCENE" +IOT_REQUEST_ID = "iot_request_id" #TODO make the id a property of the request _counter = count() @@ -57,6 +59,7 @@ class _BusKeys(): RUN = BASE + ":run." # Will have skill_id appened REGISTER = BASE + "register" CALL_FOR_REGISTRATION = REGISTER + ".request" + SPEAK = BASE + ":speak" @unique @@ -280,6 +283,33 @@ class IoTRequest(): return cls(**data) +def _track_request(func): + """ + Used within the CommonIoT skill to track IoT requests. + + The primary purpose of tracking the reqeust is determining + if the skill is currently handling an IoT request, or is + running a standard intent. While running IoT requests, certain + methods defined on MycroftSkill should behave differently than + under normal circumstances. In particular, speech related methods + should not actually trigger speech, but instead pass the message + to the IoT control skill, which will handle deconfliction (in the + event multiple skills want to respond verbally to the same request). + + Args: + func: Callable + + Returns: + Callable + + """ + @wraps(func) + def tracking_function(self, message: Message): + with self._current_request(message.data.get(IOT_REQUEST_ID)): + func(self, message) + return tracking_function + + class CommonIoTSkill(MycroftSkill, ABC): """ Skills that want to work with the CommonIoT system should @@ -305,6 +335,11 @@ class CommonIoTSkill(MycroftSkill, ABC): step on each other. """ + @wraps(MycroftSkill.__init__) + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self._current_iot_request = None + def bind(self, bus): """ Overrides MycroftSkill.bind. @@ -312,6 +347,9 @@ class CommonIoTSkill(MycroftSkill, ABC): This is called automatically during setup, and need not otherwise be used. + Subclasses that override this method must call this + via super in their implementation. + Args: bus: """ @@ -322,6 +360,18 @@ class CommonIoTSkill(MycroftSkill, ABC): self.add_event(_BusKeys.CALL_FOR_REGISTRATION, self._handle_call_for_registration) + @contextmanager + def _current_request(self, id: str): + # Multiple simultaneous requests may interfere with each other as they + # would overwrite this value, however, this seems unlikely to cause + # any real world issues and tracking multiple requests seems as + # likely to cause issues as to solve them. + self._current_iot_request = id + yield id + self._current_iot_request = None + + + @_track_request def _handle_trigger(self, message: Message): """ Given a message, determines if this skill can @@ -343,6 +393,7 @@ class CommonIoTSkill(MycroftSkill, ABC): "callback_data": callback_data}) self.bus.emit(message.response(data)) + @_track_request def _run_request(self, message: Message): """ Given a message, extracts the IoTRequest and @@ -356,6 +407,17 @@ class CommonIoTSkill(MycroftSkill, ABC): callback_data = message.data["callback_data"] self.run_request(request, callback_data) + def speak(self, utterance, *args, **kwargs): + if self._current_iot_request: + self.bus.emit(Message(_BusKeys.SPEAK, + data={"skill_id": self.skill_id, + IOT_REQUEST_ID: self._current_iot_request, + "speak_args": args, + "speak_kwargs": kwargs, + "speak": utterance})) + else: + super().speak(utterance, *args, **kwargs) + def _handle_call_for_registration(self, _: Message): """ Register this skill's scenes and entities when requested. From c966dbe5bd8cee6fa15e95f4c2f8cb4fc675d786 Mon Sep 17 00:00:00 2001 From: Chris Rogers Date: Tue, 28 May 2019 17:01:42 -0400 Subject: [PATCH 3/6] Add basic state support --- mycroft/skills/common_iot_skill.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/mycroft/skills/common_iot_skill.py b/mycroft/skills/common_iot_skill.py index aea150a396..94dedc8bc3 100644 --- a/mycroft/skills/common_iot_skill.py +++ b/mycroft/skills/common_iot_skill.py @@ -30,7 +30,7 @@ from mycroft.messagebus.message import Message ENTITY = "ENTITY" SCENE = "SCENE" -IOT_REQUEST_ID = "iot_request_id" #TODO make the id a property of the request +IOT_REQUEST_ID = "iot_request_id" # TODO make the id a property of the request _counter = count() @@ -88,6 +88,7 @@ class Attribute(Enum): BRIGHTNESS = auto() COLOR = auto() COLOR_TEMPERATURE = auto() + TEMPERATURE = auto() @unique @@ -106,8 +107,8 @@ class State(Enum): brightness or color. """ STATE = auto() - ON = auto() - OFF = auto() + POWERED = auto() + UNPOWERED = auto() LOCKED = auto() UNLOCKED = auto() OCCUPIED = auto() @@ -130,7 +131,8 @@ class Action(Enum): INCREASE = auto() DECREASE = auto() TRIGGER = auto() - QUERY = auto() + BINARY_QUERY = auto() # yes/no answer + INFORMATION_QUERY = auto() # detailed answer @total_ordering @@ -370,7 +372,6 @@ class CommonIoTSkill(MycroftSkill, ABC): yield id self._current_iot_request = None - @_track_request def _handle_trigger(self, message: Message): """ From e1a2de7046bdc33a7fabb05b7fc6f366b9ae7e12 Mon Sep 17 00:00:00 2001 From: Chris Rogers Date: Sat, 8 Jun 2019 18:22:18 -0400 Subject: [PATCH 4/6] Add locate --- mycroft/skills/common_iot_skill.py | 1 + 1 file changed, 1 insertion(+) diff --git a/mycroft/skills/common_iot_skill.py b/mycroft/skills/common_iot_skill.py index 94dedc8bc3..ea4aef1013 100644 --- a/mycroft/skills/common_iot_skill.py +++ b/mycroft/skills/common_iot_skill.py @@ -133,6 +133,7 @@ class Action(Enum): TRIGGER = auto() BINARY_QUERY = auto() # yes/no answer INFORMATION_QUERY = auto() # detailed answer + LOCATE = auto() @total_ordering From 395c31cf29bb64ca95c28e8eafd424e53f6b1bf8 Mon Sep 17 00:00:00 2001 From: Chris Rogers Date: Sat, 8 Jun 2019 18:43:05 -0400 Subject: [PATCH 5/6] Fix pep8 issues --- mycroft/skills/common_iot_skill.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/mycroft/skills/common_iot_skill.py b/mycroft/skills/common_iot_skill.py index ea4aef1013..ba01bf6306 100644 --- a/mycroft/skills/common_iot_skill.py +++ b/mycroft/skills/common_iot_skill.py @@ -243,13 +243,16 @@ class IoTRequest(): ' value={value},' ' state={state}' ')') + entity = '"{}"'.format(self.entity) if self.entity else None + scene = '"{}"'.format(self.scene) if self.scene else None + value = '"{}"'.format(self.value) if self.value is not None else None return template.format( action=self.action, thing=self.thing, attribute=self.attribute, - entity='"{}"'.format(self.entity) if self.entity else None, - scene='"{}"'.format(self.scene) if self.scene else None, - value='"{}"'.format(self.value) if self.value is not None else None, + entity=entity, + scene=scene, + value=value, state=self.state ) @@ -412,11 +415,12 @@ class CommonIoTSkill(MycroftSkill, ABC): def speak(self, utterance, *args, **kwargs): if self._current_iot_request: self.bus.emit(Message(_BusKeys.SPEAK, - data={"skill_id": self.skill_id, - IOT_REQUEST_ID: self._current_iot_request, - "speak_args": args, - "speak_kwargs": kwargs, - "speak": utterance})) + data={"skill_id": self.skill_id, + IOT_REQUEST_ID: + self._current_iot_request, + "speak_args": args, + "speak_kwargs": kwargs, + "speak": utterance})) else: super().speak(utterance, *args, **kwargs) From a40218da1838a521a19839fe766944cc84e8b069 Mon Sep 17 00:00:00 2001 From: Chris Rogers Date: Tue, 11 Jun 2019 19:58:28 -0400 Subject: [PATCH 6/6] Add lock/unlock actions --- mycroft/skills/common_iot_skill.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/mycroft/skills/common_iot_skill.py b/mycroft/skills/common_iot_skill.py index ba01bf6306..96cbf8c8ca 100644 --- a/mycroft/skills/common_iot_skill.py +++ b/mycroft/skills/common_iot_skill.py @@ -62,6 +62,11 @@ class _BusKeys(): SPEAK = BASE + ":speak" +#################################################################### +# When adding a new Thing, Attribute, etc, be sure to also add the # +# corresponding voc files to the skill-iot-control. # +#################################################################### + @unique class Thing(Enum): """ @@ -134,6 +139,8 @@ class Action(Enum): BINARY_QUERY = auto() # yes/no answer INFORMATION_QUERY = auto() # detailed answer LOCATE = auto() + LOCK = auto() + UNLOCK = auto() @total_ordering