common_iot_skill.py
Go to the documentation of this file.
1 # Copyright 2019 Mycroft AI Inc.
2 #
3 # Licensed under the Apache License, Version 2.0 (the "License");
4 # you may not use this file except in compliance with the License.
5 # You may obtain a copy of the License at
6 #
7 # http://www.apache.org/licenses/LICENSE-2.0
8 #
9 # Unless required by applicable law or agreed to in writing, software
10 # distributed under the License is distributed on an "AS IS" BASIS,
11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 # See the License for the specific language governing permissions and
13 # limitations under the License.
14 
15 
16 # THE CLASSES IN THIS FILE ARE STILL EXPERIMENTAL, AND ARE SUBJECT TO
17 # CHANGES. IT IS PROVIDED NOW AS A PREVIEW, SO SKILL AUTHORS CAN GET
18 # AN IDEA OF WHAT IS TO COME. YOU ARE FREE TO BEGIN EXPERIMENTING, BUT
19 # BE WARNED THAT THE CLASSES, FUNCTIONS, ETC MAY CHANGE WITHOUT WARNING.
20 
21 from abc import ABC, abstractmethod
22 from enum import Enum, unique
23 from functools import total_ordering
24 from itertools import count
25 
26 from mycroft import MycroftSkill
27 from mycroft.messagebus.message import Message
28 
29 
30 ENTITY = "ENTITY"
31 SCENE = "SCENE"
32 
33 
34 _counter = count()
35 
36 
37 def auto():
38  """
39  Indefinitely return the next number in sequence from 0.
40 
41  This can be replaced with enum.auto when we no longer
42  need to support python3.4.
43  """
44  return next(_counter)
45 
46 
47 class _BusKeys():
48  """
49  This class contains some strings used to identify
50  messages on the messagebus. They are used in in
51  CommonIoTSkill and the IoTController skill, but
52  are not intended to be used elsewhere.
53  """
54  BASE = "iot"
55  TRIGGER = BASE + ":trigger"
56  RESPONSE = TRIGGER + ".response"
57  RUN = BASE + ":run." # Will have skill_id appened
58  REGISTER = BASE + "register"
59  CALL_FOR_REGISTRATION = REGISTER + ".request"
60 
61 
62 @unique
63 class Thing(Enum):
64  """
65  This class represents 'Things' which may be controlled
66  by IoT Skills. This is intended to be used with the
67  IoTRequest class. See that class for more details.
68  """
69  LIGHT = auto()
70  THERMOSTAT = auto()
71  DOOR = auto()
72  LOCK = auto()
73  PLUG = auto()
74  SWITCH = auto()
75  TEMPERATURE = auto() # Control desired high and low temperatures
76  HEAT = auto() # Control desired low temperature
77  AIR_CONDITIONING = auto() # Control desired high temperature
78 
79 
80 @unique
81 class Attribute(Enum):
82  """
83  This class represents 'Attributes' of 'Things'.
84 
85  This may also grow to encompass states, e.g.
86  'locked' or 'unlocked'.
87  """
88  BRIGHTNESS = auto()
89  COLOR = auto()
90  COLOR_TEMPERATURE = auto()
91 
92 
93 @unique
94 class Action(Enum):
95  """
96  This class represents 'Actions' that can be applied to
97  'Things,' e.d. a LIGHT can be turned ON. It is intended
98  to be used with the IoTRequest class. See that class
99  for more details.
100  """
101  ON = auto()
102  OFF = auto()
103  TOGGLE = auto()
104  ADJUST = auto()
105  SET = auto()
106  INCREASE = auto()
107  DECREASE = auto()
108  TRIGGER = auto()
109 
110 
111 @total_ordering
112 class IoTRequestVersion(Enum):
113  """
114  Enum indicating support IoTRequest fields
115 
116  This class allows us to extend the request without
117  requiring that all existing skills are updated to
118  handle the new fields. Skills will simply not respond
119  to requests that contain fields they are not aware of.
120 
121  CommonIoTSkill subclasses should override
122  CommonIoTSkill.supported_request_version to indicate
123  their level of support. For backward compatibility,
124  the default is V1.
125 
126  Note that this is an attempt to avoid false positive
127  matches (i.e. prevent skills from reporting that they
128  can handle a request that contains fields they don't
129  know anything about). To avoid any possibility of
130  false negatives, however, skills should always try to
131  support the latest version.
132 
133  Version to supported fields (provided only for reference - always use the
134  latest version available, and account for all fields):
135 
136  V1 = {'action', 'thing', 'attribute', 'entity', 'scene'}
137  V2 = V1 | {'value'}
138  """
139  def __lt__(self, other):
140  return self.name < other.name
141 
142  V1 = {'action', 'thing', 'attribute', 'entity', 'scene'}
143  V2 = V1 | {'value'}
144 
145 
146 class IoTRequest():
147  """
148  This class represents a request from a user to control
149  an IoT device. It contains all of the information an IoT
150  skill should need in order to determine if it can handle
151  a user's request. The information is supplied as properties
152  on the request. At present, those properties are:
153 
154  action (see the Action enum above)
155  thing (see the Thing enum above)
156  entity
157  scene
158  value
159 
160  The 'action' is mandatory, and will always be not None. The
161  other fields may be None.
162 
163  The 'entity' is intended to be used for user-defined values
164  specific to a skill. For example, in a skill controlling Lights,
165  an 'entity' might represent a group of lights. For a smart-lock
166  skill, it might represent a specific lock, e.g. 'front door.'
167 
168  The 'scene' value is also intended to to be used for user-defined
169  values. Skills that extend CommonIotSkill are expected to register
170  their own scenes. The controller skill will have the ability to
171  trigger multiple skills, so common scene names may trigger many
172  skills, for a coherent experience.
173 
174  The 'value' property will be a number value. This is intended to
175  be used for requests such as "set the heat to 70 degrees" and
176  "set the lights to 50% brightness."
177 
178  Skills that extend CommonIotSkill will be expected to register
179  their own entities. See the documentation in CommonIotSkill for
180  more details.
181  """
182 
183  def __init__(self,
184  action: Action,
185  thing: Thing = None,
186  attribute: Attribute = None,
187  entity: str = None,
188  scene: str = None,
189  value: int = None):
190 
191  if not thing and not entity and not scene:
192  raise Exception("At least one of thing,"
193  " entity, or scene must be present!")
194 
195  self.action = action
196  self.thing = thing
197  self.attribute = attribute
198  self.entity = entity
199  self.scene = scene
200  self.value = value
201 
202  def __repr__(self):
203  template = ('IoTRequest('
204  'action={action},'
205  ' thing={thing},'
206  ' attribute={attribute},'
207  ' entity={entity},'
208  ' scene={scene},'
209  ' value={value}'
210  ')')
211  return template.format(
212  action=self.action,
213  thing=self.thing,
214  attribute=self.attribute,
215  entity='"{}"'.format(self.entity) if self.entity else None,
216  scene='"{}"'.format(self.scene) if self.scene else None,
217  value='"{}"'.format(self.value) if self.value is not None else None
218  )
219 
220  @property
221  def version(self):
222  if self.value is not None:
223  return IoTRequestVersion.V2
224  return IoTRequestVersion.V1
225 
226  def to_dict(self):
227  return {
228  'action': self.action.name,
229  'thing': self.thing.name if self.thing else None,
230  'attribute': self.attribute.name if self.attribute else None,
231  'entity': self.entity,
232  'scene': self.scene,
233  'value': self.value
234  }
235 
236  @classmethod
237  def from_dict(cls, data: dict):
238  data = data.copy()
239  data['action'] = Action[data['action']]
240  if data.get('thing') not in (None, ''):
241  data['thing'] = Thing[data['thing']]
242  if data.get('attribute') not in (None, ''):
243  data['attribute'] = Attribute[data['attribute']]
244 
245  return cls(**data)
246 
247 
248 class CommonIoTSkill(MycroftSkill, ABC):
249  """
250  Skills that want to work with the CommonIoT system should
251  extend this class. Subclasses will be expected to implement
252  two methods, `can_handle` and `run_request`. See the
253  documentation for those functions for more details on how
254  they are expected to behave.
255 
256  Subclasses may also register their own entities and scenes.
257  See the register_entities and register_scenes methods for
258  details.
259 
260  This class works in conjunction with a controller skill.
261  The controller registers vocabulary and intents to capture
262  IoT related requests. It then emits messages on the messagebus
263  that will be picked up by all skills that extend this class.
264  Each skill will have the opportunity to declare whether or not
265  it can handle the given request. Skills that acknowledge that
266  they are capable of handling the request will be considered
267  candidates, and after a short timeout, a winner, or winners,
268  will be chosen. With this setup, a user can have several IoT
269  systems, and control them all without worry that skills will
270  step on each other.
271  """
272 
273  def bind(self, bus):
274  """
275  Overrides MycroftSkill.bind.
276 
277  This is called automatically during setup, and
278  need not otherwise be used.
279 
280  Args:
281  bus:
282  """
283  if bus:
284  super().bind(bus)
285  self.add_event(_BusKeys.TRIGGER, self._handle_trigger)
286  self.add_event(_BusKeys.RUN + self.skill_id, self._run_request)
287  self.add_event(_BusKeys.CALL_FOR_REGISTRATION,
289 
290  def _handle_trigger(self, message: Message):
291  """
292  Given a message, determines if this skill can
293  handle the request. If it can, it will emit
294  a message on the bus indicating that.
295 
296  Args:
297  message: Message
298  """
299  data = message.data
300  request = IoTRequest.from_dict(data[IoTRequest.__name__])
301 
302  if request.version > self.supported_request_version:
303  return
304 
305  can_handle, callback_data = self.can_handle(request)
306  if can_handle:
307  data.update({"skill_id": self.skill_id,
308  "callback_data": callback_data})
309  self.bus.emit(message.response(data))
310 
311  def _run_request(self, message: Message):
312  """
313  Given a message, extracts the IoTRequest and
314  callback_data and sends them to the run_request
315  method.
316 
317  Args:
318  message: Message
319  """
320  request = IoTRequest.from_dict(message.data[IoTRequest.__name__])
321  callback_data = message.data["callback_data"]
322  self.run_request(request, callback_data)
323 
324  def _handle_call_for_registration(self, _: Message):
325  """
326  Register this skill's scenes and entities when requested.
327 
328  Args:
329  _: Message. This is ignored.
330  """
332 
333  def _register_words(self, words: [str], word_type: str):
334  """
335  Emit a message to the controller skill to register vocab.
336 
337  Emits a message on the bus containing the type and
338  the words. The message will be picked up by the
339  controller skill, and the vocabulary will be registered
340  to that skill.
341 
342  Args:
343  words:
344  word_type:
345  """
346  if words:
347  self.bus.emit(Message(_BusKeys.REGISTER,
348  data={"skill_id": self.skill_id,
349  "type": word_type,
350  "words": list(words)}))
351 
353  """
354  This method will register this skill's scenes and entities.
355 
356  This should be called in the skill's `initialize` method,
357  at some point after `get_entities` and `get_scenes` can
358  be expected to return correct results.
359 
360  """
361  self._register_words(self.get_entities(), ENTITY)
362  self._register_words(self.get_scenes(), SCENE)
363 
364  @property
365  def supported_request_version(self) -> IoTRequestVersion:
366  """
367  Get the supported IoTRequestVersion
368 
369  By default, this returns IoTRequestVersion.V1. Subclasses
370  should override this to indicate higher levels of support.
371 
372  The documentation for IoTRequestVersion provides a reference
373  indicating which fields are included in each version. Note
374  that you should always take the latest, and account for all
375  request fields.
376  """
377  return IoTRequestVersion.V1
378 
379  def get_entities(self) -> [str]:
380  """
381  Get a list of custom entities.
382 
383  This is intended to be overridden by subclasses, though it
384  it not required (the default implementation will return an
385  empty list).
386 
387  The strings returned by this function will be registered
388  as ENTITY values with the intent parser. Skills should provide
389  group names, user aliases for specific devices, or anything
390  else that might represent a THING or a set of THINGs, e.g.
391  'bedroom', 'lamp', 'front door.' This allows commands that
392  don't explicitly include a THING to still be handled, e.g.
393  "bedroom off" as opposed to "bedroom lights off."
394  """
395  return []
396 
397  def get_scenes(self) -> [str]:
398  """
399  Get a list of custom scenes.
400 
401  This method is intended to be overridden by subclasses, though
402  it is not required. The strings returned by this function will
403  be registered as SCENE values with the intent parser. Skills
404  should provide user defined scene names that they are aware of
405  and capable of handling, e.g. "relax," "movie time," etc.
406  """
407  return []
408 
409  @abstractmethod
410  def can_handle(self, request: IoTRequest):
411  """
412  Determine if an IoTRequest can be handled by this skill.
413 
414  This method must be implemented by all subclasses.
415 
416  An IoTRequest contains several properties (see the
417  documentation for that class). This method should return
418  True if and only if this skill can take the appropriate
419  'action' when considering _all other properties
420  of the request_. In other words, a partial match, one in which
421  any piece of the IoTRequest is not known to this skill,
422  and is not None, this should return (False, None).
423 
424  Args:
425  request: IoTRequest
426 
427  Returns: (boolean, dict)
428  True if and only if this skill knows about all the
429  properties set on the IoTRequest, and a dict containing
430  callback_data. If this skill is chosen to handle the
431  request, this dict will be supplied to `run_request`.
432 
433  Note that the dictionary will be sent over the bus, and thus
434  must be JSON serializable.
435  """
436  return False, None
437 
438  @abstractmethod
439  def run_request(self, request: IoTRequest, callback_data: dict):
440  """
441  Handle an IoT Request.
442 
443  All subclasses must implement this method.
444 
445  When this skill is chosen as a winner, this function will be called.
446  It will be passed an IoTRequest equivalent to the one that was
447  supplied to `can_handle`, as well as the `callback_data` returned by
448  `can_handle`.
449 
450  Args:
451  request: IoTRequest
452  callback_data: dict
453  """
454  pass


mycroft_ros
Author(s):
autogenerated on Mon Apr 26 2021 02:35:40