Merge pull request #2150 from MycroftAI/commonIoT/state

CommonIoT/state
pull/2166/head
Åke 2019-06-20 12:16:17 +02:00 committed by GitHub
commit 5ff6e6c89e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
1 changed files with 123 additions and 13 deletions

View File

@ -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,8 +59,14 @@ class _BusKeys():
RUN = BASE + ":run." # Will have skill_id appened
REGISTER = BASE + "register"
CALL_FOR_REGISTRATION = REGISTER + ".request"
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):
"""
@ -81,13 +89,35 @@ 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()
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()
POWERED = auto()
UNPOWERED = auto()
LOCKED = auto()
UNLOCKED = auto()
OCCUPIED = auto()
UNOCCUPIED = auto()
@unique
@ -106,6 +136,11 @@ class Action(Enum):
INCREASE = auto()
DECREASE = auto()
TRIGGER = auto()
BINARY_QUERY = auto() # yes/no answer
INFORMATION_QUERY = auto() # detailed answer
LOCATE = auto()
LOCK = auto()
UNLOCK = auto()
@total_ordering
@ -135,12 +170,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 +188,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 +225,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 +238,7 @@ class IoTRequest():
self.entity = entity
self.scene = scene
self.value = value
self.state = state
def __repr__(self):
template = ('IoTRequest('
@ -206,19 +247,26 @@ class IoTRequest():
' attribute={attribute},'
' entity={entity},'
' scene={scene},'
' value={value}'
' 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
)
@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 +278,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,10 +290,39 @@ 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)
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
@ -270,6 +348,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.
@ -277,6 +360,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:
"""
@ -287,6 +373,17 @@ 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
@ -308,6 +405,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
@ -321,6 +419,18 @@ 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.