This is a fork of the mycroft-homeassistant skill in the mycroft-skills repo. This adds new functionality, several fixes, and a much improved UX with more natural utternces and vocab.
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

197 lines
6.9 KiB

  1. from requests import get, post
  2. from fuzzywuzzy import fuzz
  3. import json
  4. from requests.exceptions import Timeout, RequestException
  5. __author__ = 'btotharye'
  6. # Timeout time for HA requests
  7. TIMEOUT = 10
  8. class HomeAssistantClient(object):
  9. def __init__(self, host, token, portnum, ssl=False, verify=True):
  10. self.ssl = ssl
  11. self.verify = verify
  12. if self.ssl:
  13. self.url = "https://{}".format(host)
  14. else:
  15. self.url = "http://{}".format(host)
  16. if portnum:
  17. self.url = "{}:{}".format(self.url, portnum)
  18. self.headers = {
  19. 'Authorization': "Bearer {}".format(token),
  20. 'Content-Type': 'application/json'
  21. }
  22. def _get_state(self):
  23. """Get state object
  24. Throws request Exceptions
  25. (Subclasses of ConnectionError or RequestException,
  26. raises HTTPErrors if non-Ok status code)
  27. """
  28. if self.ssl:
  29. req = get("{}/api/states".format(self.url), headers=self.headers,
  30. verify=self.verify, timeout=TIMEOUT)
  31. else:
  32. req = get("{}/api/states".format(self.url), headers=self.headers,
  33. timeout=TIMEOUT)
  34. req.raise_for_status()
  35. return req.json()
  36. def connected(self):
  37. try:
  38. self._get_state()
  39. return True
  40. except (Timeout, ConnectionError, RequestException):
  41. return False
  42. def find_entity(self, entity, types):
  43. """Find entity with specified name, fuzzy matching
  44. Throws request Exceptions
  45. (Subclasses of ConnectionError or RequestException,
  46. raises HTTPErrors if non-Ok status code)
  47. """
  48. json_data = self._get_state()
  49. # require a score above 50%
  50. best_score = 50
  51. best_entity = None
  52. if not json_data:
  53. return
  54. for state in json_data:
  55. try:
  56. if not state['entity_id'].split(".")[0] in types:
  57. continue
  58. # something like temperature outside
  59. # should score on "outside temperature sensor"
  60. # and repetitions should not count on my behalf
  61. for name in [
  62. state['entity_id'],
  63. state['attributes']['friendly_name']
  64. ]:
  65. score = fuzz.token_sort_ratio(
  66. entity.lower(),
  67. name.lower()
  68. )
  69. if score > best_score:
  70. best_score = score
  71. best_entity = {
  72. "id": state['entity_id'],
  73. "dev_name":
  74. state['attributes'].get(
  75. 'friendly_name',
  76. state['entity_id']
  77. ),
  78. "state": state['state'],
  79. "best_score": best_score}
  80. except KeyError:
  81. pass
  82. return best_entity
  83. def find_entity_attr(self, entity):
  84. """checking the entity attributes to be used in the response dialog.
  85. Throws request Exceptions
  86. (Subclasses of ConnectionError or RequestException,
  87. raises HTTPErrors if non-Ok status code)
  88. """
  89. json_data = self._get_state()
  90. if json_data:
  91. for attr in json_data:
  92. if attr['entity_id'] == entity:
  93. entity_attrs = attr['attributes']
  94. try:
  95. if attr['entity_id'].startswith('light.'):
  96. # Not all lamps do have a color
  97. unit_measur = entity_attrs['brightness']
  98. else:
  99. unit_measur = entity_attrs['unit_of_measurement']
  100. except KeyError:
  101. unit_measur = None
  102. # IDEA: return the color if available
  103. # TODO: change to return the whole attr dictionary =>
  104. # free use within handle methods
  105. sensor_name = entity_attrs['friendly_name']
  106. sensor_state = attr['state']
  107. entity_attr = {
  108. "unit_measure": unit_measur,
  109. "name": sensor_name,
  110. "state": sensor_state
  111. }
  112. return entity_attr
  113. return None
  114. def execute_service(self, domain, service, data):
  115. """Execute service at HAServer
  116. Throws request Exceptions
  117. (Subclasses of ConnectionError or RequestException,
  118. raises HTTPErrors if non-Ok status code)
  119. """
  120. if self.ssl:
  121. r = post("{}/api/services/{}/{}".format(self.url, domain, service),
  122. headers=self.headers, data=json.dumps(data),
  123. verify=self.verify, timeout=TIMEOUT)
  124. else:
  125. r = post("{}/api/services/{}/{}".format(self.url, domain, service),
  126. headers=self.headers, data=json.dumps(data),
  127. timeout=TIMEOUT)
  128. r.raise_for_status()
  129. return r
  130. def find_component(self, component):
  131. """Check if a component is loaded at the HA-Server
  132. Throws request Exceptions
  133. (Subclasses of ConnectionError or RequestException,
  134. raises HTTPErrors if non-Ok status code)
  135. """
  136. if self.ssl:
  137. req = get("{}/api/components".format(self.url),
  138. headers=self.headers, verify=self.verify,
  139. timeout=TIMEOUT)
  140. else:
  141. req = get("%s/api/components" % self.url, headers=self.headers,
  142. timeout=TIMEOUT)
  143. req.raise_for_status()
  144. return component in req.json()
  145. def engage_conversation(self, utterance):
  146. """Engage the conversation component at the Home Assistant server
  147. Throws request Exceptions
  148. (Subclasses of ConnectionError or RequestException,
  149. raises HTTPErrors if non-Ok status code)
  150. Attributes:
  151. utterance raw text message to be processed
  152. Return:
  153. Dict answer by Home Assistant server
  154. { 'speech': textual answer,
  155. 'extra_data': ...}
  156. """
  157. data = {
  158. "text": utterance
  159. }
  160. if self.ssl:
  161. r = post("{}/api/conversation/process".format(self.url),
  162. headers=self.headers,
  163. data=json.dumps(data),
  164. verify=self.verify,
  165. timeout=TIMEOUT
  166. )
  167. else:
  168. r = post("{}/api/conversation/process".format(self.url),
  169. headers=self.headers,
  170. data=json.dumps(data),
  171. timeout=TIMEOUT
  172. )
  173. r.raise_for_status()
  174. return r.json()['speech']['plain']