From 03adeaf0748a136d287877d0184b66b13f38c196 Mon Sep 17 00:00:00 2001 From: xiki808 Date: Sat, 6 Mar 2021 21:56:19 +0000 Subject: [PATCH 01/13] Add change light color intent --- .vscode/launch.json | 15 +++++++ __init__.py | 43 +++++++++++++++++++ .../en-us/homeassistant.color.change.dialog | 2 + vocab/en-us/change.light.color.intent | 2 + 4 files changed, 62 insertions(+) create mode 100644 .vscode/launch.json create mode 100644 dialog/en-us/homeassistant.color.change.dialog create mode 100644 vocab/en-us/change.light.color.intent diff --git a/.vscode/launch.json b/.vscode/launch.json new file mode 100644 index 00000000..a17b7e37 --- /dev/null +++ b/.vscode/launch.json @@ -0,0 +1,15 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "name": "Python: Remote Attach", + "type": "python", + "request": "attach", + "port": 5678, + "host": "localhost", + } + ] +} \ No newline at end of file diff --git a/__init__.py b/__init__.py index 74919922..dc315196 100644 --- a/__init__.py +++ b/__init__.py @@ -223,6 +223,49 @@ def handle_light_decrease_intent(self, message): message.data["Action"] = "down" self._handle_light_adjust(message) + @intent_handler('change.light.color.intent') + def handle_light_color_intent(self, message): + self.log.debug("Change light colour intent on entity: " + + message.data['entity']) + self._handle_light_color(message) + + def _handle_light_color(self, message): + self.log.debug("Starting change colour intent.") + self.log.debug("Entity: %s" % message.data["entity"]) + self.log.debug("Color: %s" % message.data["color"]) + + entity = message.data['entity'] + entity_parts = entity.split() + voc_match = entity + + # In case the utterance is for all entities + if "all" in entity_parts: + voc_match = "all lights" + + if self.voc_match(voc_match, "all_lights"): + ha_data = {'entity_id': 'all'} + ha_entity = {'dev_name': 'all lights'} + else: + ha_entity = self._find_entity(entity, ['group', 'light']) + + if not ha_entity or not self._check_availability(ha_entity): + return + + ha_data = {'entity_id': ha_entity['id']} + + color = message.data['color'] + color_parts = list(color.split()) + + # HA uses one color words, such as: lightgoldenrodyellow + if len(color_parts) > 1: + color = ''.join(color_parts) + + ha_data['color_name'] = message.data['color'] + self.ha.execute_service("light", "turn_on", ha_data) + + ha_data['dev_name'] = ha_entity['dev_name'] + self.speak_dialog('homeassistant.color.change', data=ha_data) + def _handle_turn_actions(self, message): self.log.debug("Starting Switch Intent") entity = message.data["Entity"] diff --git a/dialog/en-us/homeassistant.color.change.dialog b/dialog/en-us/homeassistant.color.change.dialog new file mode 100644 index 00000000..e8626ff0 --- /dev/null +++ b/dialog/en-us/homeassistant.color.change.dialog @@ -0,0 +1,2 @@ +Changed {{dev_name}} to {{color_name}}. +{{dev_name}} changed to {{color_name}}. diff --git a/vocab/en-us/change.light.color.intent b/vocab/en-us/change.light.color.intent new file mode 100644 index 00000000..f6729b83 --- /dev/null +++ b/vocab/en-us/change.light.color.intent @@ -0,0 +1,2 @@ +change (color (of|) |) {{entity}} to {{color}} +change {{entity}} (color|) to {{color}} \ No newline at end of file From db3de7491d1fb6f060918883dde02f7fee041801 Mon Sep 17 00:00:00 2001 From: xiki808 Date: Sat, 6 Mar 2021 22:33:19 +0000 Subject: [PATCH 02/13] Git ignore .vscode --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 137002bb..654a5eee 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,4 @@ /test/integrationtests* /settings.json /skill_developers_testrunner.py +.vscode \ No newline at end of file From f260e50ed4c13f51c80c6a1dbb5d7a938a878b3c Mon Sep 17 00:00:00 2001 From: xiki808 Date: Sat, 6 Mar 2021 22:35:44 +0000 Subject: [PATCH 03/13] Add new lines --- .gitignore | 2 +- .vscode/launch.json | 2 +- vocab/en-us/change.light.color.intent | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index 654a5eee..926f40fe 100644 --- a/.gitignore +++ b/.gitignore @@ -12,4 +12,4 @@ /test/integrationtests* /settings.json /skill_developers_testrunner.py -.vscode \ No newline at end of file +.vscode diff --git a/.vscode/launch.json b/.vscode/launch.json index a17b7e37..f5469ab1 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -12,4 +12,4 @@ "host": "localhost", } ] -} \ No newline at end of file +} diff --git a/vocab/en-us/change.light.color.intent b/vocab/en-us/change.light.color.intent index f6729b83..1cce80f5 100644 --- a/vocab/en-us/change.light.color.intent +++ b/vocab/en-us/change.light.color.intent @@ -1,2 +1,2 @@ change (color (of|) |) {{entity}} to {{color}} -change {{entity}} (color|) to {{color}} \ No newline at end of file +change {{entity}} (color|) to {{color}} From a465f2be00162538fdd6d6ac402f57dd7b155e1f Mon Sep 17 00:00:00 2001 From: xiki808 Date: Sun, 7 Mar 2021 08:53:46 +0000 Subject: [PATCH 04/13] Remove unused code - color one word not required --- __init__.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/__init__.py b/__init__.py index dc315196..d088a0cd 100644 --- a/__init__.py +++ b/__init__.py @@ -256,10 +256,6 @@ def _handle_light_color(self, message): color = message.data['color'] color_parts = list(color.split()) - # HA uses one color words, such as: lightgoldenrodyellow - if len(color_parts) > 1: - color = ''.join(color_parts) - ha_data['color_name'] = message.data['color'] self.ha.execute_service("light", "turn_on", ha_data) From 671758545fc8ff84bf2989ca5e87199f45b106ff Mon Sep 17 00:00:00 2001 From: xiki808 Date: Sun, 7 Mar 2021 09:23:35 +0000 Subject: [PATCH 05/13] Validate if device is given --- __init__.py | 5 +++++ dialog/en-us/homeassistant.device.not.given.dialog | 2 ++ 2 files changed, 7 insertions(+) create mode 100644 dialog/en-us/homeassistant.device.not.given.dialog diff --git a/__init__.py b/__init__.py index d088a0cd..c6d98204 100644 --- a/__init__.py +++ b/__init__.py @@ -225,6 +225,11 @@ def handle_light_decrease_intent(self, message): @intent_handler('change.light.color.intent') def handle_light_color_intent(self, message): + if not 'entity' in message.data: + self.speak_dialog('homeassistant.device.not.given', + data={"action": "change"}) + return + self.log.debug("Change light colour intent on entity: " + message.data['entity']) self._handle_light_color(message) diff --git a/dialog/en-us/homeassistant.device.not.given.dialog b/dialog/en-us/homeassistant.device.not.given.dialog new file mode 100644 index 00000000..2adf79a6 --- /dev/null +++ b/dialog/en-us/homeassistant.device.not.given.dialog @@ -0,0 +1,2 @@ +Please, tell me which device (to {{action}}|) +What device you want me to {{action}} \ No newline at end of file From 2c75b29993d29bb00cac05a1554a613106b6a85e Mon Sep 17 00:00:00 2001 From: xiki808 Date: Sun, 7 Mar 2021 09:30:34 +0000 Subject: [PATCH 06/13] Add new line --- dialog/en-us/homeassistant.device.not.given.dialog | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dialog/en-us/homeassistant.device.not.given.dialog b/dialog/en-us/homeassistant.device.not.given.dialog index 2adf79a6..62e7d15a 100644 --- a/dialog/en-us/homeassistant.device.not.given.dialog +++ b/dialog/en-us/homeassistant.device.not.given.dialog @@ -1,2 +1,2 @@ Please, tell me which device (to {{action}}|) -What device you want me to {{action}} \ No newline at end of file +What device you want me to {{action}} From 4b828786e5a466b96a1d83e67b18e2a0018a9fcc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Anton=C3=ADn=20Skala?= Date: Mon, 8 Mar 2021 20:51:31 +0100 Subject: [PATCH 07/13] cs-cz localization --- dialog/cs-cz/homeassistant.color.change.dialog | 2 ++ dialog/cs-cz/homeassistant.device.not.given.dialog | 2 ++ vocab/cs-cz/change.light.color.intent | 2 ++ 3 files changed, 6 insertions(+) create mode 100644 dialog/cs-cz/homeassistant.color.change.dialog create mode 100644 dialog/cs-cz/homeassistant.device.not.given.dialog create mode 100644 vocab/cs-cz/change.light.color.intent diff --git a/dialog/cs-cz/homeassistant.color.change.dialog b/dialog/cs-cz/homeassistant.color.change.dialog new file mode 100644 index 00000000..31ef1b7f --- /dev/null +++ b/dialog/cs-cz/homeassistant.color.change.dialog @@ -0,0 +1,2 @@ +Změněno {{dev_name}} na {{color_name}}. +{{dev_name}} změněno na {{color_name}}. diff --git a/dialog/cs-cz/homeassistant.device.not.given.dialog b/dialog/cs-cz/homeassistant.device.not.given.dialog new file mode 100644 index 00000000..c4e68d86 --- /dev/null +++ b/dialog/cs-cz/homeassistant.device.not.given.dialog @@ -0,0 +1,2 @@ +Prosím, řekni mi které zařízení chceš ({{action}}|) +Které zařízení chceš {{action}} diff --git a/vocab/cs-cz/change.light.color.intent b/vocab/cs-cz/change.light.color.intent new file mode 100644 index 00000000..4d132e1e --- /dev/null +++ b/vocab/cs-cz/change.light.color.intent @@ -0,0 +1,2 @@ +(změň|nastav) (barvu |) {{entity}} na {{color}} +(změň|nastav) {{entity}} na {{color}} (barvu|) From 30ecd6a0b1b68af5a9a6e775ea789f358d8c9d21 Mon Sep 17 00:00:00 2001 From: stratus-ss Date: Tue, 13 Apr 2021 12:21:57 -0400 Subject: [PATCH 08/13] Delete launch.json --- .vscode/launch.json | 15 --------------- 1 file changed, 15 deletions(-) delete mode 100644 .vscode/launch.json diff --git a/.vscode/launch.json b/.vscode/launch.json deleted file mode 100644 index f5469ab1..00000000 --- a/.vscode/launch.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - // Use IntelliSense to learn about possible attributes. - // Hover to view descriptions of existing attributes. - // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 - "version": "0.2.0", - "configurations": [ - { - "name": "Python: Remote Attach", - "type": "python", - "request": "attach", - "port": 5678, - "host": "localhost", - } - ] -} From 218727d6e2f79524a7b9fdc0466fa750a3d7ee63 Mon Sep 17 00:00:00 2001 From: stratus-ss Date: Tue, 13 Apr 2021 12:22:30 -0400 Subject: [PATCH 09/13] Delete .gitignore --- .gitignore | 15 --------------- 1 file changed, 15 deletions(-) delete mode 100644 .gitignore diff --git a/.gitignore b/.gitignore deleted file mode 100644 index 926f40fe..00000000 --- a/.gitignore +++ /dev/null @@ -1,15 +0,0 @@ -*.swp -*.pyc -.idea/misc.xml -.idea/modules.xml -.idea/mycroft-homeassistant.iml -.idea/workspace.xml -/.pydevproject -/.project -.idea/preferred-vcs.xml -/.idea/vcs.xml -/venv* -/test/integrationtests* -/settings.json -/skill_developers_testrunner.py -.vscode From f3839998c847dd16ee126709cf4aa506f4bba552 Mon Sep 17 00:00:00 2001 From: Daniel Scicluna Date: Tue, 28 Sep 2021 11:32:40 +0200 Subject: [PATCH 10/13] PR updates: cleanup and refactoring, add back gitignore --- .gitignore | 14 ++++++++++++++ __init__.py | 15 ++++++--------- 2 files changed, 20 insertions(+), 9 deletions(-) create mode 100644 .gitignore diff --git a/.gitignore b/.gitignore new file mode 100644 index 00000000..137002bb --- /dev/null +++ b/.gitignore @@ -0,0 +1,14 @@ +*.swp +*.pyc +.idea/misc.xml +.idea/modules.xml +.idea/mycroft-homeassistant.iml +.idea/workspace.xml +/.pydevproject +/.project +.idea/preferred-vcs.xml +/.idea/vcs.xml +/venv* +/test/integrationtests* +/settings.json +/skill_developers_testrunner.py diff --git a/__init__.py b/__init__.py index d1db29c2..5a3cf8b6 100644 --- a/__init__.py +++ b/__init__.py @@ -233,7 +233,7 @@ def _handle_light_color(self, message): voc_match = "all lights" if self.voc_match(voc_match, "all_lights"): - ha_data = {'entity_id': 'all'} + data_obj = {'entity_id': 'all'} ha_entity = {'dev_name': 'all lights'} else: ha_entity = self._find_entity(entity, ['group', 'light']) @@ -241,16 +241,13 @@ def _handle_light_color(self, message): if not ha_entity or not self._check_availability(ha_entity): return - ha_data = {'entity_id': ha_entity['id']} + data_obj = {'entity_id': ha_entity['id']} - color = message.data['color'] - color_parts = list(color.split()) + data_obj['color_name'] = message.data['color'] + self.ha.execute_service("light", "turn_on", data_obj) - ha_data['color_name'] = message.data['color'] - self.ha.execute_service("light", "turn_on", ha_data) - - ha_data['dev_name'] = ha_entity['dev_name'] - self.speak_dialog('homeassistant.color.change', data=ha_data) + data_obj['dev_name'] = ha_entity['dev_name'] + self.speak_dialog('homeassistant.color.change', data=data_obj) @intent_handler('automation.intent') def handle_automation_intent(self, message): From 12b562eee5d534d8ce66be821e923ffa71c4b2c7 Mon Sep 17 00:00:00 2001 From: Daniel Scicluna Date: Tue, 28 Sep 2021 11:39:49 +0200 Subject: [PATCH 11/13] Remove 'all' catch phrase --- .history/.gitignore_20210928112659 | 0 .history/.gitignore_20210928112806 | 14 + .history/__init___20210927201045.py | 636 +++++++++++++++++++++++++++ .history/__init___20210927201253.py | 645 ++++++++++++++++++++++++++++ .history/__init___20210928113153.py | 642 +++++++++++++++++++++++++++ .history/__init___20210928113925.py | 637 +++++++++++++++++++++++++++ __init__.py | 5 - 7 files changed, 2574 insertions(+), 5 deletions(-) create mode 100644 .history/.gitignore_20210928112659 create mode 100644 .history/.gitignore_20210928112806 create mode 100644 .history/__init___20210927201045.py create mode 100644 .history/__init___20210927201253.py create mode 100644 .history/__init___20210928113153.py create mode 100644 .history/__init___20210928113925.py diff --git a/.history/.gitignore_20210928112659 b/.history/.gitignore_20210928112659 new file mode 100644 index 00000000..e69de29b diff --git a/.history/.gitignore_20210928112806 b/.history/.gitignore_20210928112806 new file mode 100644 index 00000000..137002bb --- /dev/null +++ b/.history/.gitignore_20210928112806 @@ -0,0 +1,14 @@ +*.swp +*.pyc +.idea/misc.xml +.idea/modules.xml +.idea/mycroft-homeassistant.iml +.idea/workspace.xml +/.pydevproject +/.project +.idea/preferred-vcs.xml +/.idea/vcs.xml +/venv* +/test/integrationtests* +/settings.json +/skill_developers_testrunner.py diff --git a/.history/__init___20210927201045.py b/.history/__init___20210927201045.py new file mode 100644 index 00000000..eb6c4448 --- /dev/null +++ b/.history/__init___20210927201045.py @@ -0,0 +1,636 @@ +from mycroft.skills.core import FallbackSkill +from mycroft.util.format import nice_number +from mycroft import MycroftSkill, intent_handler + +from sys import exc_info + +from requests.exceptions import ( + RequestException, + Timeout, + InvalidURL, + URLRequired, + SSLError, + HTTPError) +from requests.packages.urllib3.exceptions import MaxRetryError + +from .ha_client import HomeAssistantClient + + +__author__ = 'robconnolly, btotharye, nielstron' + +# Timeout time for HA requests +TIMEOUT = 10 + + +class HomeAssistantSkill(FallbackSkill): + + def __init__(self): + MycroftSkill.__init__(self) + super().__init__(name="HomeAssistantSkill") + self.ha = None + self.enable_fallback = False + + def _setup(self, force=False): + if self.settings is not None and (force or self.ha is None): + ip = self.settings.get('host') + token = self.settings.get('token') + + # Check if user filled IP, port and Token in configuration + if not ip: + self.speak_dialog('homeassistant.error.setup', data={ + "field": "I.P."}) + return + + if not token: + self.speak_dialog('homeassistant.error.setup', data={ + "field": "token"}) + return + + portnumber = self.settings.get('portnum') + try: + portnumber = int(portnumber) + except TypeError: + portnumber = 8123 + except ValueError: + # String might be some rubbish (like '') + self.speak_dialog('homeassistant.error.setup', data={ + "field": "port"}) + return + + self.ha = HomeAssistantClient( + ip, + token, + portnumber, + self.settings.get('ssl'), + self.settings.get('verify') + ) + if self.ha.connected(): + # Check if conversation component is loaded at HA-server + # and activate fallback accordingly (ha-server/api/components) + # TODO: enable other tools like dialogflow + conversation_activated = self.ha.find_component( + 'conversation' + ) + if conversation_activated: + self.enable_fallback = \ + self.settings.get('enable_fallback') + + def _force_setup(self): + self.log.debug('Creating a new HomeAssistant-Client') + self._setup(True) + + def initialize(self): + self.language = self.config_core.get('lang') + + # Needs higher priority than general fallback skills + self.register_fallback(self.handle_fallback, 2) + # Check and then monitor for credential changes + self.settings_change_callback = self.on_websettings_changed + self._setup() + + def on_websettings_changed(self): + # Force a setting refresh after the websettings changed + # Otherwise new settings will not be regarded + self._force_setup() + + # Try to find an entity on the HAServer + # Creates dialogs for errors and speaks them + # Returns None if nothing was found + # Else returns entity that was found + def _find_entity(self, entity, domains): + self._setup() + if self.ha is None: + self.speak_dialog('homeassistant.error.setup') + return False + # TODO if entity is 'all', 'any' or 'every' turn on + # every single entity not the whole group + ha_entity = self._handle_client_exception(self.ha.find_entity, + entity, domains) + if ha_entity is None: + self.speak_dialog('homeassistant.device.unknown', data={ + "dev_name": entity}) + return ha_entity + + # Routine for entiti availibility check + def _check_availability(self, ha_entity): + """ Simple routine for checking availability of entity inside + Home Assistent. """ + + if ha_entity['state'] == 'unavailable': + """ Check if state is `unavailable`, if yes, inform user about it. """ + + self.speak_dialog('homeassistant.device.unavailable', data={ + "dev_name": ha_entity['dev_name']}) + """ Return result to underliing function. """ + return False + return True + + # Calls passed method and catches often occurring exceptions + def _handle_client_exception(self, callback, *args, **kwargs): + try: + return callback(*args, **kwargs) + except Timeout: + self.speak_dialog('homeassistant.error.offline') + except (InvalidURL, URLRequired, MaxRetryError) as e: + if e.request is None or e.request.url is None: + # There is no url configured + self.speak_dialog('homeassistant.error.needurl') + else: + self.speak_dialog('homeassistant.error.invalidurl', data={ + 'url': e.request.url}) + except SSLError: + self.speak_dialog('homeassistant.error.ssl') + except HTTPError as e: + # check if due to wrong password + if e.response.status_code == 401: + self.speak_dialog('homeassistant.error.wrong_password') + else: + self.speak_dialog('homeassistant.error.http', data={ + 'code': e.response.status_code, + 'reason': e.response.reason}) + except (ConnectionError, RequestException) as exception: + # TODO find a nice member of any exception to output + self.speak_dialog('homeassistant.error', data={ + 'url': exception.request.url}) + + return False + + # Intent handlers + @intent_handler('turn.on.intent') + def handle_turn_on_intent(self, message): + self.log.debug("Turn on intent on entity: "+message.data.get("entity")) + message.data["Entity"] = message.data.get("entity") + message.data["Action"] = "on" + self._handle_turn_actions(message) + + @intent_handler('turn.off.intent') + def handle_turn_off_intent(self, message): + self.log.debug(message.data) + self.log.debug("Turn off intent on entity: "+message.data.get("entity")) + message.data["Entity"] = message.data.get("entity") + message.data["Action"] = "off" + self._handle_turn_actions(message) + + @intent_handler('toggle.intent') + def handle_toggle_intent(self, message): + self.log.debug("Toggle intent on entity: " + message.data.get("entity")) + message.data["Entity"] = message.data.get("entity") + message.data["Action"] = "toggle" + self._handle_turn_actions(message) + + @intent_handler('sensor.intent') + def handle_sensor_intent(self, message): + self.log.debug("Turn on intent on entity: "+message.data.get("entity")) + message.data["Entity"] = message.data.get("entity") + self._handle_sensor(message) + + @intent_handler('set.light.brightness.intent') + def handle_light_set_intent(self, message): + self.log.debug("Change light intensity: "+message.data.get("entity") \ + +"to"+message.data.get("brightnessvalue")+"percent") + message.data["Entity"] = message.data.get("entity") + message.data["Brightnessvalue"] = message.data.get("brightnessvalue") + self._handle_light_set(message) + + @intent_handler('increase.light.brightness.intent') + def handle_light_increase_intent(self, message): + self.log.debug("Increase light intensity: "+message.data.get("entity")) + message.data["Entity"] = message.data.get("entity") + message.data["Action"] = "up" + self._handle_light_adjust(message) + + @intent_handler('decrease.light.brightness.intent') + def handle_light_decrease_intent(self, message): + self.log.debug("Decrease light intensity: "+message.data.get("entity")) + message.data["Entity"] = message.data.get("entity") + message.data["Action"] = "down" + self._handle_light_adjust(message) + +<<<<<<< HEAD + @intent_handler('change.light.color.intent') + def handle_light_color_intent(self, message): + if not 'entity' in message.data: + self.speak_dialog('homeassistant.device.not.given', + data={"action": "change"}) + return + + self.log.debug("Change light colour intent on entity: " + + message.data['entity']) + self._handle_light_color(message) + + def _handle_light_color(self, message): + self.log.debug("Starting change colour intent.") + self.log.debug("Entity: %s" % message.data["entity"]) + self.log.debug("Color: %s" % message.data["color"]) + + entity = message.data['entity'] + entity_parts = entity.split() + voc_match = entity + + # In case the utterance is for all entities + if "all" in entity_parts: + voc_match = "all lights" + + if self.voc_match(voc_match, "all_lights"): + ha_data = {'entity_id': 'all'} + ha_entity = {'dev_name': 'all lights'} + else: + ha_entity = self._find_entity(entity, ['group', 'light']) + + if not ha_entity or not self._check_availability(ha_entity): + return + + ha_data = {'entity_id': ha_entity['id']} + + color = message.data['color'] + color_parts = list(color.split()) + + ha_data['color_name'] = message.data['color'] + self.ha.execute_service("light", "turn_on", ha_data) + + ha_data['dev_name'] = ha_entity['dev_name'] + self.speak_dialog('homeassistant.color.change', data=ha_data) +======= + @intent_handler('automation.intent') + def handle_automation_intent(self, message): + self.log.debug("Automation trigger intent on entity: "+message.data.get("entity")) + message.data["Entity"] = message.data.get("entity") + self._handle_automation(message) + + @intent_handler('tracker.intent') + def handle_tracker_intent(self, message): + self.log.debug("Turn on intent on entity: "+message.data.get("entity")) + message.data["Entity"] = message.data.get("entity") + self._handle_tracker(message) + + @intent_handler('set.climate.intent') + def handle_set_thermostat_intent(self, message): + self.log.debug("Set thermostat intent on entity: "+message.data.get("entity")) + message.data["Entity"] = message.data.get("entity") + message.data["Temp"] = message.data.get("temp") + self._handle_set_thermostat(message) + + @intent_handler('add.item.shopping.list.intent') + def handle_shopping_list_intent(self, message): + self.log.debug("Add : "+message.data.get("entity")+"to the shoping list") + message.data["Entity"] = message.data.get("entity") + self._handle_shopping_list(message) +>>>>>>> 2f8b7a452d22fe7abd9b53b89cb38af42c90837f + + def _handle_turn_actions(self, message): + self.log.debug("Starting Switch Intent") + entity = message.data["Entity"] + action = message.data["Action"] + self.log.debug("Entity: %s" % entity) + self.log.debug("Action: %s" % action) + + # Handle turn on/off all intent + try: + if self.voc_match(entity,"all_lights"): + domain = "light" + elif self.voc_match(entity,"all_switches"): + domain = "switch" + else: + domain = None + + if domain is not None: + ha_entity = {'dev_name': entity} + ha_data = {'entity_id': 'all'} + + self.ha.execute_service(domain, "turn_%s" % action, ha_data) + self.speak_dialog('homeassistant.device.%s' % action, data=ha_entity) + return + # TODO: need to figure out, if this indeed throws a KeyError + except KeyError: + self.log.debug("Not turn on/off all intent") + except: + self.log.debug("Unexpected error in turn all intent:", exc_info()[0]) + + # Hande single entity + + ha_entity = self._find_entity( + entity, + [ + 'group', + 'light', + 'fan', + 'switch', + 'scene', + 'input_boolean', + 'climate' + ] + ) + + # Exit if entiti not found or is unavailabe + if not ha_entity or not self._check_availability(ha_entity): + return + + self.log.debug("Entity State: %s" % ha_entity['state']) + + ha_data = {'entity_id': ha_entity['id']} + + # IDEA: set context for 'turn it off' again or similar + # self.set_context('Entity', ha_entity['dev_name']) + if ha_entity['state'] == action: + self.log.debug("Entity in requested state") + self.speak_dialog('homeassistant.device.already', data={ + "dev_name": ha_entity['dev_name'], 'action': action}) + elif action == "toggle": + self.ha.execute_service("homeassistant", "toggle", + ha_data) + if(ha_entity['state'] == 'off'): + action = 'on' + else: + action = 'off' + self.speak_dialog('homeassistant.device.%s' % action, + data=ha_entity) + elif action in ["on", "off"]: + self.speak_dialog('homeassistant.device.%s' % action, + data=ha_entity) + self.ha.execute_service("homeassistant", "turn_%s" % action, + ha_data) + else: + self.speak_dialog('homeassistant.error.sorry') + return + + def _handle_light_set(self, message): + entity = message.data["entity"] + try: + brightness_req = float(message.data["Brightnessvalue"]) + if brightness_req > 100 or brightness_req < 0: + self.speak_dialog('homeassistant.brightness.badreq') + except KeyError: + brightness_req = 10.0 + brightness_value = int(brightness_req / 100 * 255) + brightness_percentage = int(brightness_req) + self.log.debug("Entity: %s" % entity) + self.log.debug("Brightness Value: %s" % brightness_value) + self.log.debug("Brightness Percent: %s" % brightness_percentage) + + ha_entity = self._find_entity(entity, ['group', 'light']) + # Exit if entiti not found or is unavailabe + if not ha_entity or not self._check_availability(ha_entity): + return + + ha_data = {'entity_id': ha_entity['id']} + + # IDEA: set context for 'turn it off again' or similar + # self.set_context('Entity', ha_entity['dev_name']) + # Set values for HA + ha_data['brightness'] = brightness_value + self.ha.execute_service("light", "turn_on", ha_data) + # Set values for mycroft reply + ha_data['dev_name'] = ha_entity['dev_name'] + ha_data['brightness'] = brightness_req + self.speak_dialog('homeassistant.brightness.dimmed', + data=ha_data) + + return + + def _handle_shopping_list(self, message): + entity = message.data["Entity"] + ha_data = {'name': entity} + self.ha.execute_service("shopping_list", "add_item", ha_data) + self.speak_dialog("homeassistant.shopping.list") + return + + def _handle_light_adjust(self, message): + entity = message.data["Entity"] + action = message.data["Action"] + brightness_req = 10.0 + brightness_value = int(brightness_req / 100 * 255) + # brightness_percentage = int(brightness_req) # debating use + self.log.debug("Entity: %s" % entity) + self.log.debug("Brightness Value: %s" % brightness_value) + + # Set the min and max brightness for bulbs. Smart bulbs + # use 0-255 integer brightness, while spoken commands will + # use 0-100% brightness. + min_brightness = 5 + max_brightness = 255 + + ha_entity = self._find_entity(entity, ['group', 'light']) + # Exit if entiti not found or is unavailabe + if not ha_entity or not self._check_availability(ha_entity): + return + ha_data = {'entity_id': ha_entity['id']} + # IDEA: set context for 'turn it off again' or similar + # self.set_context('Entity', ha_entity['dev_name']) + + if action == "down": + if ha_entity['state'] == "off": + self.speak_dialog('homeassistant.brightness.cantdim.off', + data=ha_entity) + else: + light_attrs = self.ha.find_entity_attr(ha_entity['id']) + if light_attrs['unit_measure'] is None: + self.speak_dialog( + 'homeassistant.brightness.cantdim.dimmable', + data=ha_entity) + else: + ha_data['brightness'] = light_attrs['unit_measure'] - brightness_value + if ha_data['brightness'] < min_brightness: + ha_data['brightness'] = min_brightness + self.ha.execute_service("homeassistant", + "turn_on", + ha_data) + ha_data['dev_name'] = ha_entity['dev_name'] + ha_data['brightness'] = round(100 / max_brightness * ha_data['brightness']) + self.speak_dialog('homeassistant.brightness.decreased', + data=ha_data) + elif action == "up": + if ha_entity['state'] == "off": + self.speak_dialog( + 'homeassistant.brightness.cantdim.off', + data=ha_entity) + else: + light_attrs = self.ha.find_entity_attr(ha_entity['id']) + if light_attrs['unit_measure'] is None: + self.speak_dialog( + 'homeassistant.brightness.cantdim.dimmable', + data=ha_entity) + else: + ha_data['brightness'] = light_attrs['unit_measure'] + brightness_value + if ha_data['brightness'] > max_brightness: + ha_data['brightness'] = max_brightness + self.ha.execute_service("homeassistant", + "turn_on", + ha_data) + ha_data['dev_name'] = ha_entity['dev_name'] + ha_data['brightness'] = round(100 / max_brightness * ha_data['brightness']) + self.speak_dialog('homeassistant.brightness.increased', + data=ha_data) + else: + self.speak_dialog('homeassistant.error.sorry') + return + + def _handle_automation(self, message): + entity = message.data["Entity"] + self.log.debug("Entity: %s" % entity) + ha_entity = self._find_entity( + entity, + ['automation', 'scene', 'script'] + ) + + # Exit if entiti not found or is unavailabe + if not ha_entity or not self._check_availability(ha_entity): + return + + ha_data = {'entity_id': ha_entity['id']} + + # IDEA: set context for 'turn it off again' or similar + # self.set_context('Entity', ha_entity['dev_name']) + + self.log.debug("Triggered automation/scene/script: {}".format(ha_data)) + if "automation" in ha_entity['id']: + self.ha.execute_service('automation', 'trigger', ha_data) + self.speak_dialog('homeassistant.automation.trigger', + data={"dev_name": ha_entity['dev_name']}) + elif "script" in ha_entity['id']: + self.speak_dialog('homeassistant.automation.trigger', + data={"dev_name": ha_entity['dev_name']}) + self.ha.execute_service("script", "turn_on", + data=ha_data) + elif "scene" in ha_entity['id']: + self.speak_dialog('homeassistant.scene.on', + data=ha_entity) + self.ha.execute_service("scene", "turn_on", + data=ha_data) + + def _handle_sensor(self, message): + entity = message.data["Entity"] + self.log.debug("Entity: %s" % entity) + + ha_entity = self._find_entity(entity, ['sensor', 'switch']) + # Exit if entiti not found or is unavailabe + if not ha_entity or not self._check_availability(ha_entity): + return + + entity = ha_entity['id'] + + # IDEA: set context for 'read it out again' or similar + # self.set_context('Entity', ha_entity['dev_name']) + + unit_measurement = self.ha.find_entity_attr(entity) + sensor_unit = unit_measurement.get('unit_measure') or '' + + sensor_name = unit_measurement['name'] + sensor_state = unit_measurement['state'] + # extract unit for correct pronounciation + # this is fully optional + try: + from quantulum3 import parser + quantulumImport = True + except ImportError: + quantulumImport = False + + if quantulumImport and unit_measurement != '': + quantity = parser.parse((u'{} is {} {}'.format( + sensor_name, sensor_state, sensor_unit))) + if len(quantity) > 0: + quantity = quantity[0] + if (quantity.unit.name != "dimensionless" and + quantity.uncertainty <= 0.5): + sensor_unit = quantity.unit.name + sensor_state = quantity.value + + try: + value = float(sensor_state) + sensor_state = nice_number(value, lang=self.language) + except ValueError: + pass + + self.speak_dialog('homeassistant.sensor', data={ + "dev_name": sensor_name, + "value": sensor_state, + "unit": sensor_unit}) + # IDEA: Add some context if the person wants to look the unit up + # Maybe also change to name + # if one wants to look up "outside temperature" + # self.set_context("SubjectOfInterest", sensor_unit) + + # In progress, still testing. + # Device location works. + # Proximity might be an issue + # - overlapping command for directions modules + # - (e.g. "How far is x from y?") + def _handle_tracker(self, message): + entity = message.data["Entity"] + self.log.debug("Entity: %s" % entity) + + ha_entity = self._find_entity(entity, ['device_tracker']) + # Exit if entiti not found or is unavailabe + if not ha_entity or not self._check_availability(ha_entity): + return + + # IDEA: set context for 'locate it again' or similar + # self.set_context('Entity', ha_entity['dev_name']) + + entity = ha_entity['id'] + dev_name = ha_entity['dev_name'] + dev_location = ha_entity['state'] + self.speak_dialog('homeassistant.tracker.found', + data={'dev_name': dev_name, + 'location': dev_location}) + + def _handle_set_thermostat(self, message): + entity = message.data["entity"] + self.log.debug("Entity: %s" % entity) + self.log.debug("This is the message data: %s" % message.data) + temperature = message.data["temp"] + self.log.debug("Temperature: %s" % temperature) + + ha_entity = self._find_entity(entity, ['climate']) + # Exit if entiti not found or is unavailabe + if not ha_entity or not self._check_availability(ha_entity): + return + + climate_data = { + 'entity_id': ha_entity['id'], + 'temperature': temperature + } + climate_attr = self.ha.find_entity_attr(ha_entity['id']) + self.ha.execute_service("climate", "set_temperature", + data=climate_data) + self.speak_dialog('homeassistant.set.thermostat', + data={ + "dev_name": climate_attr['name'], + "value": temperature, + "unit": climate_attr['unit_measure']}) + + def handle_fallback(self, message): + if not self.enable_fallback: + return False + self._setup() + if self.ha is None: + self.speak_dialog('homeassistant.error.setup') + return False + # pass message to HA-server + response = self._handle_client_exception( + self.ha.engage_conversation, + message.data.get('utterance')) + if not response: + return False + # default non-parsing answer: "Sorry, I didn't understand that" + answer = response.get('speech') + if not answer or answer == "Sorry, I didn't understand that": + return False + + asked_question = False + # TODO: maybe enable conversation here if server asks sth like + # "In which room?" => answer should be directly passed to this skill + if answer.endswith("?"): + asked_question = True + self.speak(answer, expect_response=asked_question) + return True + + def shutdown(self): + self.remove_fallback(self.handle_fallback) + super(HomeAssistantSkill, self).shutdown() + + def stop(self): + pass + + +def create_skill(): + return HomeAssistantSkill() diff --git a/.history/__init___20210927201253.py b/.history/__init___20210927201253.py new file mode 100644 index 00000000..d1db29c2 --- /dev/null +++ b/.history/__init___20210927201253.py @@ -0,0 +1,645 @@ +from mycroft.skills.core import FallbackSkill +from mycroft.util.format import nice_number +from mycroft import MycroftSkill, intent_handler + +from sys import exc_info + +from requests.exceptions import ( + RequestException, + Timeout, + InvalidURL, + URLRequired, + SSLError, + HTTPError) +from requests.packages.urllib3.exceptions import MaxRetryError + +from .ha_client import HomeAssistantClient + + +__author__ = 'robconnolly, btotharye, nielstron' + +# Timeout time for HA requests +TIMEOUT = 10 + + +class HomeAssistantSkill(FallbackSkill): + + def __init__(self): + MycroftSkill.__init__(self) + super().__init__(name="HomeAssistantSkill") + self.ha = None + self.enable_fallback = False + + def _setup(self, force=False): + if self.settings is not None and (force or self.ha is None): + ip = self.settings.get('host') + token = self.settings.get('token') + + # Check if user filled IP, port and Token in configuration + if not ip: + self.speak_dialog('homeassistant.error.setup', data={ + "field": "I.P."}) + return + + if not token: + self.speak_dialog('homeassistant.error.setup', data={ + "field": "token"}) + return + + portnumber = self.settings.get('portnum') + try: + portnumber = int(portnumber) + except TypeError: + portnumber = 8123 + except ValueError: + # String might be some rubbish (like '') + self.speak_dialog('homeassistant.error.setup', data={ + "field": "port"}) + return + + self.ha = HomeAssistantClient( + ip, + token, + portnumber, + self.settings.get('ssl'), + self.settings.get('verify') + ) + if self.ha.connected(): + # Check if conversation component is loaded at HA-server + # and activate fallback accordingly (ha-server/api/components) + # TODO: enable other tools like dialogflow + conversation_activated = self.ha.find_component( + 'conversation' + ) + if conversation_activated: + self.enable_fallback = \ + self.settings.get('enable_fallback') + + def _force_setup(self): + self.log.debug('Creating a new HomeAssistant-Client') + self._setup(True) + + def initialize(self): + self.language = self.config_core.get('lang') + + # Needs higher priority than general fallback skills + self.register_fallback(self.handle_fallback, 2) + # Check and then monitor for credential changes + self.settings_change_callback = self.on_websettings_changed + self._setup() + + def on_websettings_changed(self): + # Force a setting refresh after the websettings changed + # Otherwise new settings will not be regarded + self._force_setup() + + # Try to find an entity on the HAServer + # Creates dialogs for errors and speaks them + # Returns None if nothing was found + # Else returns entity that was found + def _find_entity(self, entity, domains): + self._setup() + if self.ha is None: + self.speak_dialog('homeassistant.error.setup') + return False + # TODO if entity is 'all', 'any' or 'every' turn on + # every single entity not the whole group + ha_entity = self._handle_client_exception(self.ha.find_entity, + entity, domains) + if ha_entity is None: + self.speak_dialog('homeassistant.device.unknown', data={ + "dev_name": entity}) + return ha_entity + + # Routine for entiti availibility check + def _check_availability(self, ha_entity): + """ Simple routine for checking availability of entity inside + Home Assistent. """ + + if ha_entity['state'] == 'unavailable': + """ Check if state is `unavailable`, if yes, inform user about it. """ + + self.speak_dialog('homeassistant.device.unavailable', data={ + "dev_name": ha_entity['dev_name']}) + """ Return result to underliing function. """ + return False + return True + + # Calls passed method and catches often occurring exceptions + def _handle_client_exception(self, callback, *args, **kwargs): + try: + return callback(*args, **kwargs) + except Timeout: + self.speak_dialog('homeassistant.error.offline') + except (InvalidURL, URLRequired, MaxRetryError) as e: + if e.request is None or e.request.url is None: + # There is no url configured + self.speak_dialog('homeassistant.error.needurl') + else: + self.speak_dialog('homeassistant.error.invalidurl', data={ + 'url': e.request.url}) + except SSLError: + self.speak_dialog('homeassistant.error.ssl') + except HTTPError as e: + # check if due to wrong password + if e.response.status_code == 401: + self.speak_dialog('homeassistant.error.wrong_password') + else: + self.speak_dialog('homeassistant.error.http', data={ + 'code': e.response.status_code, + 'reason': e.response.reason}) + except (ConnectionError, RequestException) as exception: + # TODO find a nice member of any exception to output + self.speak_dialog('homeassistant.error', data={ + 'url': exception.request.url}) + + return False + + # Intent handlers + @intent_handler('turn.on.intent') + def handle_turn_on_intent(self, message): + self.log.debug("Turn on intent on entity: "+message.data.get("entity")) + message.data["Entity"] = message.data.get("entity") + message.data["Action"] = "on" + self._handle_turn_actions(message) + + @intent_handler('turn.off.intent') + def handle_turn_off_intent(self, message): + self.log.debug(message.data) + self.log.debug("Turn off intent on entity: " + + message.data.get("entity")) + message.data["Entity"] = message.data.get("entity") + message.data["Action"] = "off" + self._handle_turn_actions(message) + + @intent_handler('toggle.intent') + def handle_toggle_intent(self, message): + self.log.debug("Toggle intent on entity: " + + message.data.get("entity")) + message.data["Entity"] = message.data.get("entity") + message.data["Action"] = "toggle" + self._handle_turn_actions(message) + + @intent_handler('sensor.intent') + def handle_sensor_intent(self, message): + self.log.debug("Turn on intent on entity: "+message.data.get("entity")) + message.data["Entity"] = message.data.get("entity") + self._handle_sensor(message) + + @intent_handler('set.light.brightness.intent') + def handle_light_set_intent(self, message): + self.log.debug("Change light intensity: "+message.data.get("entity") + + "to"+message.data.get("brightnessvalue")+"percent") + message.data["Entity"] = message.data.get("entity") + message.data["Brightnessvalue"] = message.data.get("brightnessvalue") + self._handle_light_set(message) + + @intent_handler('increase.light.brightness.intent') + def handle_light_increase_intent(self, message): + self.log.debug("Increase light intensity: "+message.data.get("entity")) + message.data["Entity"] = message.data.get("entity") + message.data["Action"] = "up" + self._handle_light_adjust(message) + + @intent_handler('decrease.light.brightness.intent') + def handle_light_decrease_intent(self, message): + self.log.debug("Decrease light intensity: "+message.data.get("entity")) + message.data["Entity"] = message.data.get("entity") + message.data["Action"] = "down" + self._handle_light_adjust(message) + + @intent_handler('change.light.color.intent') + def handle_light_color_intent(self, message): + if not 'entity' in message.data: + self.speak_dialog('homeassistant.device.not.given', + data={"action": "change"}) + return + + self.log.debug("Change light colour intent on entity: " + + message.data['entity']) + self._handle_light_color(message) + + def _handle_light_color(self, message): + self.log.debug("Starting change colour intent.") + self.log.debug("Entity: %s" % message.data["entity"]) + self.log.debug("Color: %s" % message.data["color"]) + + entity = message.data['entity'] + entity_parts = entity.split() + voc_match = entity + + # In case the utterance is for all entities + if "all" in entity_parts: + voc_match = "all lights" + + if self.voc_match(voc_match, "all_lights"): + ha_data = {'entity_id': 'all'} + ha_entity = {'dev_name': 'all lights'} + else: + ha_entity = self._find_entity(entity, ['group', 'light']) + + if not ha_entity or not self._check_availability(ha_entity): + return + + ha_data = {'entity_id': ha_entity['id']} + + color = message.data['color'] + color_parts = list(color.split()) + + ha_data['color_name'] = message.data['color'] + self.ha.execute_service("light", "turn_on", ha_data) + + ha_data['dev_name'] = ha_entity['dev_name'] + self.speak_dialog('homeassistant.color.change', data=ha_data) + + @intent_handler('automation.intent') + def handle_automation_intent(self, message): + self.log.debug("Automation trigger intent on entity: " + + message.data.get("entity")) + message.data["Entity"] = message.data.get("entity") + self._handle_automation(message) + + @intent_handler('tracker.intent') + def handle_tracker_intent(self, message): + self.log.debug("Turn on intent on entity: "+message.data.get("entity")) + message.data["Entity"] = message.data.get("entity") + self._handle_tracker(message) + + @intent_handler('set.climate.intent') + def handle_set_thermostat_intent(self, message): + self.log.debug("Set thermostat intent on entity: " + + message.data.get("entity")) + message.data["Entity"] = message.data.get("entity") + message.data["Temp"] = message.data.get("temp") + self._handle_set_thermostat(message) + + @intent_handler('add.item.shopping.list.intent') + def handle_shopping_list_intent(self, message): + self.log.debug("Add : "+message.data.get("entity") + + "to the shoping list") + message.data["Entity"] = message.data.get("entity") + self._handle_shopping_list(message) + + def _handle_turn_actions(self, message): + self.log.debug("Starting Switch Intent") + entity = message.data["Entity"] + action = message.data["Action"] + self.log.debug("Entity: %s" % entity) + self.log.debug("Action: %s" % action) + + # Handle turn on/off all intent + try: + if self.voc_match(entity, "all_lights"): + domain = "light" + elif self.voc_match(entity, "all_switches"): + domain = "switch" + else: + domain = None + + if domain is not None: + ha_entity = {'dev_name': entity} + ha_data = {'entity_id': 'all'} + + self.ha.execute_service(domain, "turn_%s" % action, ha_data) + self.speak_dialog('homeassistant.device.%s' % + action, data=ha_entity) + return + # TODO: need to figure out, if this indeed throws a KeyError + except KeyError: + self.log.debug("Not turn on/off all intent") + except: + self.log.debug( + "Unexpected error in turn all intent:", exc_info()[0]) + + # Hande single entity + + ha_entity = self._find_entity( + entity, + [ + 'group', + 'light', + 'fan', + 'switch', + 'scene', + 'input_boolean', + 'climate' + ] + ) + + # Exit if entiti not found or is unavailabe + if not ha_entity or not self._check_availability(ha_entity): + return + + self.log.debug("Entity State: %s" % ha_entity['state']) + + ha_data = {'entity_id': ha_entity['id']} + + # IDEA: set context for 'turn it off' again or similar + # self.set_context('Entity', ha_entity['dev_name']) + if ha_entity['state'] == action: + self.log.debug("Entity in requested state") + self.speak_dialog('homeassistant.device.already', data={ + "dev_name": ha_entity['dev_name'], 'action': action}) + elif action == "toggle": + self.ha.execute_service("homeassistant", "toggle", + ha_data) + if(ha_entity['state'] == 'off'): + action = 'on' + else: + action = 'off' + self.speak_dialog('homeassistant.device.%s' % action, + data=ha_entity) + elif action in ["on", "off"]: + self.speak_dialog('homeassistant.device.%s' % action, + data=ha_entity) + self.ha.execute_service("homeassistant", "turn_%s" % action, + ha_data) + else: + self.speak_dialog('homeassistant.error.sorry') + return + + def _handle_light_set(self, message): + entity = message.data["entity"] + try: + brightness_req = float(message.data["Brightnessvalue"]) + if brightness_req > 100 or brightness_req < 0: + self.speak_dialog('homeassistant.brightness.badreq') + except KeyError: + brightness_req = 10.0 + brightness_value = int(brightness_req / 100 * 255) + brightness_percentage = int(brightness_req) + self.log.debug("Entity: %s" % entity) + self.log.debug("Brightness Value: %s" % brightness_value) + self.log.debug("Brightness Percent: %s" % brightness_percentage) + + ha_entity = self._find_entity(entity, ['group', 'light']) + # Exit if entiti not found or is unavailabe + if not ha_entity or not self._check_availability(ha_entity): + return + + ha_data = {'entity_id': ha_entity['id']} + + # IDEA: set context for 'turn it off again' or similar + # self.set_context('Entity', ha_entity['dev_name']) + # Set values for HA + ha_data['brightness'] = brightness_value + self.ha.execute_service("light", "turn_on", ha_data) + # Set values for mycroft reply + ha_data['dev_name'] = ha_entity['dev_name'] + ha_data['brightness'] = brightness_req + self.speak_dialog('homeassistant.brightness.dimmed', + data=ha_data) + + return + + def _handle_shopping_list(self, message): + entity = message.data["Entity"] + ha_data = {'name': entity} + self.ha.execute_service("shopping_list", "add_item", ha_data) + self.speak_dialog("homeassistant.shopping.list") + return + + def _handle_light_adjust(self, message): + entity = message.data["Entity"] + action = message.data["Action"] + brightness_req = 10.0 + brightness_value = int(brightness_req / 100 * 255) + # brightness_percentage = int(brightness_req) # debating use + self.log.debug("Entity: %s" % entity) + self.log.debug("Brightness Value: %s" % brightness_value) + + # Set the min and max brightness for bulbs. Smart bulbs + # use 0-255 integer brightness, while spoken commands will + # use 0-100% brightness. + min_brightness = 5 + max_brightness = 255 + + ha_entity = self._find_entity(entity, ['group', 'light']) + # Exit if entiti not found or is unavailabe + if not ha_entity or not self._check_availability(ha_entity): + return + ha_data = {'entity_id': ha_entity['id']} + # IDEA: set context for 'turn it off again' or similar + # self.set_context('Entity', ha_entity['dev_name']) + + if action == "down": + if ha_entity['state'] == "off": + self.speak_dialog('homeassistant.brightness.cantdim.off', + data=ha_entity) + else: + light_attrs = self.ha.find_entity_attr(ha_entity['id']) + if light_attrs['unit_measure'] is None: + self.speak_dialog( + 'homeassistant.brightness.cantdim.dimmable', + data=ha_entity) + else: + ha_data['brightness'] = light_attrs['unit_measure'] - \ + brightness_value + if ha_data['brightness'] < min_brightness: + ha_data['brightness'] = min_brightness + self.ha.execute_service("homeassistant", + "turn_on", + ha_data) + ha_data['dev_name'] = ha_entity['dev_name'] + ha_data['brightness'] = round( + 100 / max_brightness * ha_data['brightness']) + self.speak_dialog('homeassistant.brightness.decreased', + data=ha_data) + elif action == "up": + if ha_entity['state'] == "off": + self.speak_dialog( + 'homeassistant.brightness.cantdim.off', + data=ha_entity) + else: + light_attrs = self.ha.find_entity_attr(ha_entity['id']) + if light_attrs['unit_measure'] is None: + self.speak_dialog( + 'homeassistant.brightness.cantdim.dimmable', + data=ha_entity) + else: + ha_data['brightness'] = light_attrs['unit_measure'] + \ + brightness_value + if ha_data['brightness'] > max_brightness: + ha_data['brightness'] = max_brightness + self.ha.execute_service("homeassistant", + "turn_on", + ha_data) + ha_data['dev_name'] = ha_entity['dev_name'] + ha_data['brightness'] = round( + 100 / max_brightness * ha_data['brightness']) + self.speak_dialog('homeassistant.brightness.increased', + data=ha_data) + else: + self.speak_dialog('homeassistant.error.sorry') + return + + def _handle_automation(self, message): + entity = message.data["Entity"] + self.log.debug("Entity: %s" % entity) + ha_entity = self._find_entity( + entity, + ['automation', 'scene', 'script'] + ) + + # Exit if entiti not found or is unavailabe + if not ha_entity or not self._check_availability(ha_entity): + return + + ha_data = {'entity_id': ha_entity['id']} + + # IDEA: set context for 'turn it off again' or similar + # self.set_context('Entity', ha_entity['dev_name']) + + self.log.debug("Triggered automation/scene/script: {}".format(ha_data)) + if "automation" in ha_entity['id']: + self.ha.execute_service('automation', 'trigger', ha_data) + self.speak_dialog('homeassistant.automation.trigger', + data={"dev_name": ha_entity['dev_name']}) + elif "script" in ha_entity['id']: + self.speak_dialog('homeassistant.automation.trigger', + data={"dev_name": ha_entity['dev_name']}) + self.ha.execute_service("script", "turn_on", + data=ha_data) + elif "scene" in ha_entity['id']: + self.speak_dialog('homeassistant.scene.on', + data=ha_entity) + self.ha.execute_service("scene", "turn_on", + data=ha_data) + + def _handle_sensor(self, message): + entity = message.data["Entity"] + self.log.debug("Entity: %s" % entity) + + ha_entity = self._find_entity(entity, ['sensor', 'switch']) + # Exit if entiti not found or is unavailabe + if not ha_entity or not self._check_availability(ha_entity): + return + + entity = ha_entity['id'] + + # IDEA: set context for 'read it out again' or similar + # self.set_context('Entity', ha_entity['dev_name']) + + unit_measurement = self.ha.find_entity_attr(entity) + sensor_unit = unit_measurement.get('unit_measure') or '' + + sensor_name = unit_measurement['name'] + sensor_state = unit_measurement['state'] + # extract unit for correct pronounciation + # this is fully optional + try: + from quantulum3 import parser + quantulumImport = True + except ImportError: + quantulumImport = False + + if quantulumImport and unit_measurement != '': + quantity = parser.parse((u'{} is {} {}'.format( + sensor_name, sensor_state, sensor_unit))) + if len(quantity) > 0: + quantity = quantity[0] + if (quantity.unit.name != "dimensionless" and + quantity.uncertainty <= 0.5): + sensor_unit = quantity.unit.name + sensor_state = quantity.value + + try: + value = float(sensor_state) + sensor_state = nice_number(value, lang=self.language) + except ValueError: + pass + + self.speak_dialog('homeassistant.sensor', data={ + "dev_name": sensor_name, + "value": sensor_state, + "unit": sensor_unit}) + # IDEA: Add some context if the person wants to look the unit up + # Maybe also change to name + # if one wants to look up "outside temperature" + # self.set_context("SubjectOfInterest", sensor_unit) + + # In progress, still testing. + # Device location works. + # Proximity might be an issue + # - overlapping command for directions modules + # - (e.g. "How far is x from y?") + def _handle_tracker(self, message): + entity = message.data["Entity"] + self.log.debug("Entity: %s" % entity) + + ha_entity = self._find_entity(entity, ['device_tracker']) + # Exit if entiti not found or is unavailabe + if not ha_entity or not self._check_availability(ha_entity): + return + + # IDEA: set context for 'locate it again' or similar + # self.set_context('Entity', ha_entity['dev_name']) + + entity = ha_entity['id'] + dev_name = ha_entity['dev_name'] + dev_location = ha_entity['state'] + self.speak_dialog('homeassistant.tracker.found', + data={'dev_name': dev_name, + 'location': dev_location}) + + def _handle_set_thermostat(self, message): + entity = message.data["entity"] + self.log.debug("Entity: %s" % entity) + self.log.debug("This is the message data: %s" % message.data) + temperature = message.data["temp"] + self.log.debug("Temperature: %s" % temperature) + + ha_entity = self._find_entity(entity, ['climate']) + # Exit if entiti not found or is unavailabe + if not ha_entity or not self._check_availability(ha_entity): + return + + climate_data = { + 'entity_id': ha_entity['id'], + 'temperature': temperature + } + climate_attr = self.ha.find_entity_attr(ha_entity['id']) + self.ha.execute_service("climate", "set_temperature", + data=climate_data) + self.speak_dialog('homeassistant.set.thermostat', + data={ + "dev_name": climate_attr['name'], + "value": temperature, + "unit": climate_attr['unit_measure']}) + + def handle_fallback(self, message): + if not self.enable_fallback: + return False + self._setup() + if self.ha is None: + self.speak_dialog('homeassistant.error.setup') + return False + # pass message to HA-server + response = self._handle_client_exception( + self.ha.engage_conversation, + message.data.get('utterance')) + if not response: + return False + # default non-parsing answer: "Sorry, I didn't understand that" + answer = response.get('speech') + if not answer or answer == "Sorry, I didn't understand that": + return False + + asked_question = False + # TODO: maybe enable conversation here if server asks sth like + # "In which room?" => answer should be directly passed to this skill + if answer.endswith("?"): + asked_question = True + self.speak(answer, expect_response=asked_question) + return True + + def shutdown(self): + self.remove_fallback(self.handle_fallback) + super(HomeAssistantSkill, self).shutdown() + + def stop(self): + pass + + +def create_skill(): + return HomeAssistantSkill() diff --git a/.history/__init___20210928113153.py b/.history/__init___20210928113153.py new file mode 100644 index 00000000..5a3cf8b6 --- /dev/null +++ b/.history/__init___20210928113153.py @@ -0,0 +1,642 @@ +from mycroft.skills.core import FallbackSkill +from mycroft.util.format import nice_number +from mycroft import MycroftSkill, intent_handler + +from sys import exc_info + +from requests.exceptions import ( + RequestException, + Timeout, + InvalidURL, + URLRequired, + SSLError, + HTTPError) +from requests.packages.urllib3.exceptions import MaxRetryError + +from .ha_client import HomeAssistantClient + + +__author__ = 'robconnolly, btotharye, nielstron' + +# Timeout time for HA requests +TIMEOUT = 10 + + +class HomeAssistantSkill(FallbackSkill): + + def __init__(self): + MycroftSkill.__init__(self) + super().__init__(name="HomeAssistantSkill") + self.ha = None + self.enable_fallback = False + + def _setup(self, force=False): + if self.settings is not None and (force or self.ha is None): + ip = self.settings.get('host') + token = self.settings.get('token') + + # Check if user filled IP, port and Token in configuration + if not ip: + self.speak_dialog('homeassistant.error.setup', data={ + "field": "I.P."}) + return + + if not token: + self.speak_dialog('homeassistant.error.setup', data={ + "field": "token"}) + return + + portnumber = self.settings.get('portnum') + try: + portnumber = int(portnumber) + except TypeError: + portnumber = 8123 + except ValueError: + # String might be some rubbish (like '') + self.speak_dialog('homeassistant.error.setup', data={ + "field": "port"}) + return + + self.ha = HomeAssistantClient( + ip, + token, + portnumber, + self.settings.get('ssl'), + self.settings.get('verify') + ) + if self.ha.connected(): + # Check if conversation component is loaded at HA-server + # and activate fallback accordingly (ha-server/api/components) + # TODO: enable other tools like dialogflow + conversation_activated = self.ha.find_component( + 'conversation' + ) + if conversation_activated: + self.enable_fallback = \ + self.settings.get('enable_fallback') + + def _force_setup(self): + self.log.debug('Creating a new HomeAssistant-Client') + self._setup(True) + + def initialize(self): + self.language = self.config_core.get('lang') + + # Needs higher priority than general fallback skills + self.register_fallback(self.handle_fallback, 2) + # Check and then monitor for credential changes + self.settings_change_callback = self.on_websettings_changed + self._setup() + + def on_websettings_changed(self): + # Force a setting refresh after the websettings changed + # Otherwise new settings will not be regarded + self._force_setup() + + # Try to find an entity on the HAServer + # Creates dialogs for errors and speaks them + # Returns None if nothing was found + # Else returns entity that was found + def _find_entity(self, entity, domains): + self._setup() + if self.ha is None: + self.speak_dialog('homeassistant.error.setup') + return False + # TODO if entity is 'all', 'any' or 'every' turn on + # every single entity not the whole group + ha_entity = self._handle_client_exception(self.ha.find_entity, + entity, domains) + if ha_entity is None: + self.speak_dialog('homeassistant.device.unknown', data={ + "dev_name": entity}) + return ha_entity + + # Routine for entiti availibility check + def _check_availability(self, ha_entity): + """ Simple routine for checking availability of entity inside + Home Assistent. """ + + if ha_entity['state'] == 'unavailable': + """ Check if state is `unavailable`, if yes, inform user about it. """ + + self.speak_dialog('homeassistant.device.unavailable', data={ + "dev_name": ha_entity['dev_name']}) + """ Return result to underliing function. """ + return False + return True + + # Calls passed method and catches often occurring exceptions + def _handle_client_exception(self, callback, *args, **kwargs): + try: + return callback(*args, **kwargs) + except Timeout: + self.speak_dialog('homeassistant.error.offline') + except (InvalidURL, URLRequired, MaxRetryError) as e: + if e.request is None or e.request.url is None: + # There is no url configured + self.speak_dialog('homeassistant.error.needurl') + else: + self.speak_dialog('homeassistant.error.invalidurl', data={ + 'url': e.request.url}) + except SSLError: + self.speak_dialog('homeassistant.error.ssl') + except HTTPError as e: + # check if due to wrong password + if e.response.status_code == 401: + self.speak_dialog('homeassistant.error.wrong_password') + else: + self.speak_dialog('homeassistant.error.http', data={ + 'code': e.response.status_code, + 'reason': e.response.reason}) + except (ConnectionError, RequestException) as exception: + # TODO find a nice member of any exception to output + self.speak_dialog('homeassistant.error', data={ + 'url': exception.request.url}) + + return False + + # Intent handlers + @intent_handler('turn.on.intent') + def handle_turn_on_intent(self, message): + self.log.debug("Turn on intent on entity: "+message.data.get("entity")) + message.data["Entity"] = message.data.get("entity") + message.data["Action"] = "on" + self._handle_turn_actions(message) + + @intent_handler('turn.off.intent') + def handle_turn_off_intent(self, message): + self.log.debug(message.data) + self.log.debug("Turn off intent on entity: " + + message.data.get("entity")) + message.data["Entity"] = message.data.get("entity") + message.data["Action"] = "off" + self._handle_turn_actions(message) + + @intent_handler('toggle.intent') + def handle_toggle_intent(self, message): + self.log.debug("Toggle intent on entity: " + + message.data.get("entity")) + message.data["Entity"] = message.data.get("entity") + message.data["Action"] = "toggle" + self._handle_turn_actions(message) + + @intent_handler('sensor.intent') + def handle_sensor_intent(self, message): + self.log.debug("Turn on intent on entity: "+message.data.get("entity")) + message.data["Entity"] = message.data.get("entity") + self._handle_sensor(message) + + @intent_handler('set.light.brightness.intent') + def handle_light_set_intent(self, message): + self.log.debug("Change light intensity: "+message.data.get("entity") + + "to"+message.data.get("brightnessvalue")+"percent") + message.data["Entity"] = message.data.get("entity") + message.data["Brightnessvalue"] = message.data.get("brightnessvalue") + self._handle_light_set(message) + + @intent_handler('increase.light.brightness.intent') + def handle_light_increase_intent(self, message): + self.log.debug("Increase light intensity: "+message.data.get("entity")) + message.data["Entity"] = message.data.get("entity") + message.data["Action"] = "up" + self._handle_light_adjust(message) + + @intent_handler('decrease.light.brightness.intent') + def handle_light_decrease_intent(self, message): + self.log.debug("Decrease light intensity: "+message.data.get("entity")) + message.data["Entity"] = message.data.get("entity") + message.data["Action"] = "down" + self._handle_light_adjust(message) + + @intent_handler('change.light.color.intent') + def handle_light_color_intent(self, message): + if not 'entity' in message.data: + self.speak_dialog('homeassistant.device.not.given', + data={"action": "change"}) + return + + self.log.debug("Change light colour intent on entity: " + + message.data['entity']) + self._handle_light_color(message) + + def _handle_light_color(self, message): + self.log.debug("Starting change colour intent.") + self.log.debug("Entity: %s" % message.data["entity"]) + self.log.debug("Color: %s" % message.data["color"]) + + entity = message.data['entity'] + entity_parts = entity.split() + voc_match = entity + + # In case the utterance is for all entities + if "all" in entity_parts: + voc_match = "all lights" + + if self.voc_match(voc_match, "all_lights"): + data_obj = {'entity_id': 'all'} + ha_entity = {'dev_name': 'all lights'} + else: + ha_entity = self._find_entity(entity, ['group', 'light']) + + if not ha_entity or not self._check_availability(ha_entity): + return + + data_obj = {'entity_id': ha_entity['id']} + + data_obj['color_name'] = message.data['color'] + self.ha.execute_service("light", "turn_on", data_obj) + + data_obj['dev_name'] = ha_entity['dev_name'] + self.speak_dialog('homeassistant.color.change', data=data_obj) + + @intent_handler('automation.intent') + def handle_automation_intent(self, message): + self.log.debug("Automation trigger intent on entity: " + + message.data.get("entity")) + message.data["Entity"] = message.data.get("entity") + self._handle_automation(message) + + @intent_handler('tracker.intent') + def handle_tracker_intent(self, message): + self.log.debug("Turn on intent on entity: "+message.data.get("entity")) + message.data["Entity"] = message.data.get("entity") + self._handle_tracker(message) + + @intent_handler('set.climate.intent') + def handle_set_thermostat_intent(self, message): + self.log.debug("Set thermostat intent on entity: " + + message.data.get("entity")) + message.data["Entity"] = message.data.get("entity") + message.data["Temp"] = message.data.get("temp") + self._handle_set_thermostat(message) + + @intent_handler('add.item.shopping.list.intent') + def handle_shopping_list_intent(self, message): + self.log.debug("Add : "+message.data.get("entity") + + "to the shoping list") + message.data["Entity"] = message.data.get("entity") + self._handle_shopping_list(message) + + def _handle_turn_actions(self, message): + self.log.debug("Starting Switch Intent") + entity = message.data["Entity"] + action = message.data["Action"] + self.log.debug("Entity: %s" % entity) + self.log.debug("Action: %s" % action) + + # Handle turn on/off all intent + try: + if self.voc_match(entity, "all_lights"): + domain = "light" + elif self.voc_match(entity, "all_switches"): + domain = "switch" + else: + domain = None + + if domain is not None: + ha_entity = {'dev_name': entity} + ha_data = {'entity_id': 'all'} + + self.ha.execute_service(domain, "turn_%s" % action, ha_data) + self.speak_dialog('homeassistant.device.%s' % + action, data=ha_entity) + return + # TODO: need to figure out, if this indeed throws a KeyError + except KeyError: + self.log.debug("Not turn on/off all intent") + except: + self.log.debug( + "Unexpected error in turn all intent:", exc_info()[0]) + + # Hande single entity + + ha_entity = self._find_entity( + entity, + [ + 'group', + 'light', + 'fan', + 'switch', + 'scene', + 'input_boolean', + 'climate' + ] + ) + + # Exit if entiti not found or is unavailabe + if not ha_entity or not self._check_availability(ha_entity): + return + + self.log.debug("Entity State: %s" % ha_entity['state']) + + ha_data = {'entity_id': ha_entity['id']} + + # IDEA: set context for 'turn it off' again or similar + # self.set_context('Entity', ha_entity['dev_name']) + if ha_entity['state'] == action: + self.log.debug("Entity in requested state") + self.speak_dialog('homeassistant.device.already', data={ + "dev_name": ha_entity['dev_name'], 'action': action}) + elif action == "toggle": + self.ha.execute_service("homeassistant", "toggle", + ha_data) + if(ha_entity['state'] == 'off'): + action = 'on' + else: + action = 'off' + self.speak_dialog('homeassistant.device.%s' % action, + data=ha_entity) + elif action in ["on", "off"]: + self.speak_dialog('homeassistant.device.%s' % action, + data=ha_entity) + self.ha.execute_service("homeassistant", "turn_%s" % action, + ha_data) + else: + self.speak_dialog('homeassistant.error.sorry') + return + + def _handle_light_set(self, message): + entity = message.data["entity"] + try: + brightness_req = float(message.data["Brightnessvalue"]) + if brightness_req > 100 or brightness_req < 0: + self.speak_dialog('homeassistant.brightness.badreq') + except KeyError: + brightness_req = 10.0 + brightness_value = int(brightness_req / 100 * 255) + brightness_percentage = int(brightness_req) + self.log.debug("Entity: %s" % entity) + self.log.debug("Brightness Value: %s" % brightness_value) + self.log.debug("Brightness Percent: %s" % brightness_percentage) + + ha_entity = self._find_entity(entity, ['group', 'light']) + # Exit if entiti not found or is unavailabe + if not ha_entity or not self._check_availability(ha_entity): + return + + ha_data = {'entity_id': ha_entity['id']} + + # IDEA: set context for 'turn it off again' or similar + # self.set_context('Entity', ha_entity['dev_name']) + # Set values for HA + ha_data['brightness'] = brightness_value + self.ha.execute_service("light", "turn_on", ha_data) + # Set values for mycroft reply + ha_data['dev_name'] = ha_entity['dev_name'] + ha_data['brightness'] = brightness_req + self.speak_dialog('homeassistant.brightness.dimmed', + data=ha_data) + + return + + def _handle_shopping_list(self, message): + entity = message.data["Entity"] + ha_data = {'name': entity} + self.ha.execute_service("shopping_list", "add_item", ha_data) + self.speak_dialog("homeassistant.shopping.list") + return + + def _handle_light_adjust(self, message): + entity = message.data["Entity"] + action = message.data["Action"] + brightness_req = 10.0 + brightness_value = int(brightness_req / 100 * 255) + # brightness_percentage = int(brightness_req) # debating use + self.log.debug("Entity: %s" % entity) + self.log.debug("Brightness Value: %s" % brightness_value) + + # Set the min and max brightness for bulbs. Smart bulbs + # use 0-255 integer brightness, while spoken commands will + # use 0-100% brightness. + min_brightness = 5 + max_brightness = 255 + + ha_entity = self._find_entity(entity, ['group', 'light']) + # Exit if entiti not found or is unavailabe + if not ha_entity or not self._check_availability(ha_entity): + return + ha_data = {'entity_id': ha_entity['id']} + # IDEA: set context for 'turn it off again' or similar + # self.set_context('Entity', ha_entity['dev_name']) + + if action == "down": + if ha_entity['state'] == "off": + self.speak_dialog('homeassistant.brightness.cantdim.off', + data=ha_entity) + else: + light_attrs = self.ha.find_entity_attr(ha_entity['id']) + if light_attrs['unit_measure'] is None: + self.speak_dialog( + 'homeassistant.brightness.cantdim.dimmable', + data=ha_entity) + else: + ha_data['brightness'] = light_attrs['unit_measure'] - \ + brightness_value + if ha_data['brightness'] < min_brightness: + ha_data['brightness'] = min_brightness + self.ha.execute_service("homeassistant", + "turn_on", + ha_data) + ha_data['dev_name'] = ha_entity['dev_name'] + ha_data['brightness'] = round( + 100 / max_brightness * ha_data['brightness']) + self.speak_dialog('homeassistant.brightness.decreased', + data=ha_data) + elif action == "up": + if ha_entity['state'] == "off": + self.speak_dialog( + 'homeassistant.brightness.cantdim.off', + data=ha_entity) + else: + light_attrs = self.ha.find_entity_attr(ha_entity['id']) + if light_attrs['unit_measure'] is None: + self.speak_dialog( + 'homeassistant.brightness.cantdim.dimmable', + data=ha_entity) + else: + ha_data['brightness'] = light_attrs['unit_measure'] + \ + brightness_value + if ha_data['brightness'] > max_brightness: + ha_data['brightness'] = max_brightness + self.ha.execute_service("homeassistant", + "turn_on", + ha_data) + ha_data['dev_name'] = ha_entity['dev_name'] + ha_data['brightness'] = round( + 100 / max_brightness * ha_data['brightness']) + self.speak_dialog('homeassistant.brightness.increased', + data=ha_data) + else: + self.speak_dialog('homeassistant.error.sorry') + return + + def _handle_automation(self, message): + entity = message.data["Entity"] + self.log.debug("Entity: %s" % entity) + ha_entity = self._find_entity( + entity, + ['automation', 'scene', 'script'] + ) + + # Exit if entiti not found or is unavailabe + if not ha_entity or not self._check_availability(ha_entity): + return + + ha_data = {'entity_id': ha_entity['id']} + + # IDEA: set context for 'turn it off again' or similar + # self.set_context('Entity', ha_entity['dev_name']) + + self.log.debug("Triggered automation/scene/script: {}".format(ha_data)) + if "automation" in ha_entity['id']: + self.ha.execute_service('automation', 'trigger', ha_data) + self.speak_dialog('homeassistant.automation.trigger', + data={"dev_name": ha_entity['dev_name']}) + elif "script" in ha_entity['id']: + self.speak_dialog('homeassistant.automation.trigger', + data={"dev_name": ha_entity['dev_name']}) + self.ha.execute_service("script", "turn_on", + data=ha_data) + elif "scene" in ha_entity['id']: + self.speak_dialog('homeassistant.scene.on', + data=ha_entity) + self.ha.execute_service("scene", "turn_on", + data=ha_data) + + def _handle_sensor(self, message): + entity = message.data["Entity"] + self.log.debug("Entity: %s" % entity) + + ha_entity = self._find_entity(entity, ['sensor', 'switch']) + # Exit if entiti not found or is unavailabe + if not ha_entity or not self._check_availability(ha_entity): + return + + entity = ha_entity['id'] + + # IDEA: set context for 'read it out again' or similar + # self.set_context('Entity', ha_entity['dev_name']) + + unit_measurement = self.ha.find_entity_attr(entity) + sensor_unit = unit_measurement.get('unit_measure') or '' + + sensor_name = unit_measurement['name'] + sensor_state = unit_measurement['state'] + # extract unit for correct pronounciation + # this is fully optional + try: + from quantulum3 import parser + quantulumImport = True + except ImportError: + quantulumImport = False + + if quantulumImport and unit_measurement != '': + quantity = parser.parse((u'{} is {} {}'.format( + sensor_name, sensor_state, sensor_unit))) + if len(quantity) > 0: + quantity = quantity[0] + if (quantity.unit.name != "dimensionless" and + quantity.uncertainty <= 0.5): + sensor_unit = quantity.unit.name + sensor_state = quantity.value + + try: + value = float(sensor_state) + sensor_state = nice_number(value, lang=self.language) + except ValueError: + pass + + self.speak_dialog('homeassistant.sensor', data={ + "dev_name": sensor_name, + "value": sensor_state, + "unit": sensor_unit}) + # IDEA: Add some context if the person wants to look the unit up + # Maybe also change to name + # if one wants to look up "outside temperature" + # self.set_context("SubjectOfInterest", sensor_unit) + + # In progress, still testing. + # Device location works. + # Proximity might be an issue + # - overlapping command for directions modules + # - (e.g. "How far is x from y?") + def _handle_tracker(self, message): + entity = message.data["Entity"] + self.log.debug("Entity: %s" % entity) + + ha_entity = self._find_entity(entity, ['device_tracker']) + # Exit if entiti not found or is unavailabe + if not ha_entity or not self._check_availability(ha_entity): + return + + # IDEA: set context for 'locate it again' or similar + # self.set_context('Entity', ha_entity['dev_name']) + + entity = ha_entity['id'] + dev_name = ha_entity['dev_name'] + dev_location = ha_entity['state'] + self.speak_dialog('homeassistant.tracker.found', + data={'dev_name': dev_name, + 'location': dev_location}) + + def _handle_set_thermostat(self, message): + entity = message.data["entity"] + self.log.debug("Entity: %s" % entity) + self.log.debug("This is the message data: %s" % message.data) + temperature = message.data["temp"] + self.log.debug("Temperature: %s" % temperature) + + ha_entity = self._find_entity(entity, ['climate']) + # Exit if entiti not found or is unavailabe + if not ha_entity or not self._check_availability(ha_entity): + return + + climate_data = { + 'entity_id': ha_entity['id'], + 'temperature': temperature + } + climate_attr = self.ha.find_entity_attr(ha_entity['id']) + self.ha.execute_service("climate", "set_temperature", + data=climate_data) + self.speak_dialog('homeassistant.set.thermostat', + data={ + "dev_name": climate_attr['name'], + "value": temperature, + "unit": climate_attr['unit_measure']}) + + def handle_fallback(self, message): + if not self.enable_fallback: + return False + self._setup() + if self.ha is None: + self.speak_dialog('homeassistant.error.setup') + return False + # pass message to HA-server + response = self._handle_client_exception( + self.ha.engage_conversation, + message.data.get('utterance')) + if not response: + return False + # default non-parsing answer: "Sorry, I didn't understand that" + answer = response.get('speech') + if not answer or answer == "Sorry, I didn't understand that": + return False + + asked_question = False + # TODO: maybe enable conversation here if server asks sth like + # "In which room?" => answer should be directly passed to this skill + if answer.endswith("?"): + asked_question = True + self.speak(answer, expect_response=asked_question) + return True + + def shutdown(self): + self.remove_fallback(self.handle_fallback) + super(HomeAssistantSkill, self).shutdown() + + def stop(self): + pass + + +def create_skill(): + return HomeAssistantSkill() diff --git a/.history/__init___20210928113925.py b/.history/__init___20210928113925.py new file mode 100644 index 00000000..80ceb3d9 --- /dev/null +++ b/.history/__init___20210928113925.py @@ -0,0 +1,637 @@ +from mycroft.skills.core import FallbackSkill +from mycroft.util.format import nice_number +from mycroft import MycroftSkill, intent_handler + +from sys import exc_info + +from requests.exceptions import ( + RequestException, + Timeout, + InvalidURL, + URLRequired, + SSLError, + HTTPError) +from requests.packages.urllib3.exceptions import MaxRetryError + +from .ha_client import HomeAssistantClient + + +__author__ = 'robconnolly, btotharye, nielstron' + +# Timeout time for HA requests +TIMEOUT = 10 + + +class HomeAssistantSkill(FallbackSkill): + + def __init__(self): + MycroftSkill.__init__(self) + super().__init__(name="HomeAssistantSkill") + self.ha = None + self.enable_fallback = False + + def _setup(self, force=False): + if self.settings is not None and (force or self.ha is None): + ip = self.settings.get('host') + token = self.settings.get('token') + + # Check if user filled IP, port and Token in configuration + if not ip: + self.speak_dialog('homeassistant.error.setup', data={ + "field": "I.P."}) + return + + if not token: + self.speak_dialog('homeassistant.error.setup', data={ + "field": "token"}) + return + + portnumber = self.settings.get('portnum') + try: + portnumber = int(portnumber) + except TypeError: + portnumber = 8123 + except ValueError: + # String might be some rubbish (like '') + self.speak_dialog('homeassistant.error.setup', data={ + "field": "port"}) + return + + self.ha = HomeAssistantClient( + ip, + token, + portnumber, + self.settings.get('ssl'), + self.settings.get('verify') + ) + if self.ha.connected(): + # Check if conversation component is loaded at HA-server + # and activate fallback accordingly (ha-server/api/components) + # TODO: enable other tools like dialogflow + conversation_activated = self.ha.find_component( + 'conversation' + ) + if conversation_activated: + self.enable_fallback = \ + self.settings.get('enable_fallback') + + def _force_setup(self): + self.log.debug('Creating a new HomeAssistant-Client') + self._setup(True) + + def initialize(self): + self.language = self.config_core.get('lang') + + # Needs higher priority than general fallback skills + self.register_fallback(self.handle_fallback, 2) + # Check and then monitor for credential changes + self.settings_change_callback = self.on_websettings_changed + self._setup() + + def on_websettings_changed(self): + # Force a setting refresh after the websettings changed + # Otherwise new settings will not be regarded + self._force_setup() + + # Try to find an entity on the HAServer + # Creates dialogs for errors and speaks them + # Returns None if nothing was found + # Else returns entity that was found + def _find_entity(self, entity, domains): + self._setup() + if self.ha is None: + self.speak_dialog('homeassistant.error.setup') + return False + # TODO if entity is 'all', 'any' or 'every' turn on + # every single entity not the whole group + ha_entity = self._handle_client_exception(self.ha.find_entity, + entity, domains) + if ha_entity is None: + self.speak_dialog('homeassistant.device.unknown', data={ + "dev_name": entity}) + return ha_entity + + # Routine for entiti availibility check + def _check_availability(self, ha_entity): + """ Simple routine for checking availability of entity inside + Home Assistent. """ + + if ha_entity['state'] == 'unavailable': + """ Check if state is `unavailable`, if yes, inform user about it. """ + + self.speak_dialog('homeassistant.device.unavailable', data={ + "dev_name": ha_entity['dev_name']}) + """ Return result to underliing function. """ + return False + return True + + # Calls passed method and catches often occurring exceptions + def _handle_client_exception(self, callback, *args, **kwargs): + try: + return callback(*args, **kwargs) + except Timeout: + self.speak_dialog('homeassistant.error.offline') + except (InvalidURL, URLRequired, MaxRetryError) as e: + if e.request is None or e.request.url is None: + # There is no url configured + self.speak_dialog('homeassistant.error.needurl') + else: + self.speak_dialog('homeassistant.error.invalidurl', data={ + 'url': e.request.url}) + except SSLError: + self.speak_dialog('homeassistant.error.ssl') + except HTTPError as e: + # check if due to wrong password + if e.response.status_code == 401: + self.speak_dialog('homeassistant.error.wrong_password') + else: + self.speak_dialog('homeassistant.error.http', data={ + 'code': e.response.status_code, + 'reason': e.response.reason}) + except (ConnectionError, RequestException) as exception: + # TODO find a nice member of any exception to output + self.speak_dialog('homeassistant.error', data={ + 'url': exception.request.url}) + + return False + + # Intent handlers + @intent_handler('turn.on.intent') + def handle_turn_on_intent(self, message): + self.log.debug("Turn on intent on entity: "+message.data.get("entity")) + message.data["Entity"] = message.data.get("entity") + message.data["Action"] = "on" + self._handle_turn_actions(message) + + @intent_handler('turn.off.intent') + def handle_turn_off_intent(self, message): + self.log.debug(message.data) + self.log.debug("Turn off intent on entity: " + + message.data.get("entity")) + message.data["Entity"] = message.data.get("entity") + message.data["Action"] = "off" + self._handle_turn_actions(message) + + @intent_handler('toggle.intent') + def handle_toggle_intent(self, message): + self.log.debug("Toggle intent on entity: " + + message.data.get("entity")) + message.data["Entity"] = message.data.get("entity") + message.data["Action"] = "toggle" + self._handle_turn_actions(message) + + @intent_handler('sensor.intent') + def handle_sensor_intent(self, message): + self.log.debug("Turn on intent on entity: "+message.data.get("entity")) + message.data["Entity"] = message.data.get("entity") + self._handle_sensor(message) + + @intent_handler('set.light.brightness.intent') + def handle_light_set_intent(self, message): + self.log.debug("Change light intensity: "+message.data.get("entity") + + "to"+message.data.get("brightnessvalue")+"percent") + message.data["Entity"] = message.data.get("entity") + message.data["Brightnessvalue"] = message.data.get("brightnessvalue") + self._handle_light_set(message) + + @intent_handler('increase.light.brightness.intent') + def handle_light_increase_intent(self, message): + self.log.debug("Increase light intensity: "+message.data.get("entity")) + message.data["Entity"] = message.data.get("entity") + message.data["Action"] = "up" + self._handle_light_adjust(message) + + @intent_handler('decrease.light.brightness.intent') + def handle_light_decrease_intent(self, message): + self.log.debug("Decrease light intensity: "+message.data.get("entity")) + message.data["Entity"] = message.data.get("entity") + message.data["Action"] = "down" + self._handle_light_adjust(message) + + @intent_handler('change.light.color.intent') + def handle_light_color_intent(self, message): + if not 'entity' in message.data: + self.speak_dialog('homeassistant.device.not.given', + data={"action": "change"}) + return + + self.log.debug("Change light colour intent on entity: " + + message.data['entity']) + self._handle_light_color(message) + + def _handle_light_color(self, message): + self.log.debug("Starting change colour intent.") + self.log.debug("Entity: %s" % message.data["entity"]) + self.log.debug("Color: %s" % message.data["color"]) + + entity = message.data['entity'] + voc_match = entity + + if self.voc_match(voc_match, "all_lights"): + data_obj = {'entity_id': 'all'} + ha_entity = {'dev_name': 'all lights'} + else: + ha_entity = self._find_entity(entity, ['group', 'light']) + + if not ha_entity or not self._check_availability(ha_entity): + return + + data_obj = {'entity_id': ha_entity['id']} + + data_obj['color_name'] = message.data['color'] + self.ha.execute_service("light", "turn_on", data_obj) + + data_obj['dev_name'] = ha_entity['dev_name'] + self.speak_dialog('homeassistant.color.change', data=data_obj) + + @intent_handler('automation.intent') + def handle_automation_intent(self, message): + self.log.debug("Automation trigger intent on entity: " + + message.data.get("entity")) + message.data["Entity"] = message.data.get("entity") + self._handle_automation(message) + + @intent_handler('tracker.intent') + def handle_tracker_intent(self, message): + self.log.debug("Turn on intent on entity: "+message.data.get("entity")) + message.data["Entity"] = message.data.get("entity") + self._handle_tracker(message) + + @intent_handler('set.climate.intent') + def handle_set_thermostat_intent(self, message): + self.log.debug("Set thermostat intent on entity: " + + message.data.get("entity")) + message.data["Entity"] = message.data.get("entity") + message.data["Temp"] = message.data.get("temp") + self._handle_set_thermostat(message) + + @intent_handler('add.item.shopping.list.intent') + def handle_shopping_list_intent(self, message): + self.log.debug("Add : "+message.data.get("entity") + + "to the shoping list") + message.data["Entity"] = message.data.get("entity") + self._handle_shopping_list(message) + + def _handle_turn_actions(self, message): + self.log.debug("Starting Switch Intent") + entity = message.data["Entity"] + action = message.data["Action"] + self.log.debug("Entity: %s" % entity) + self.log.debug("Action: %s" % action) + + # Handle turn on/off all intent + try: + if self.voc_match(entity, "all_lights"): + domain = "light" + elif self.voc_match(entity, "all_switches"): + domain = "switch" + else: + domain = None + + if domain is not None: + ha_entity = {'dev_name': entity} + ha_data = {'entity_id': 'all'} + + self.ha.execute_service(domain, "turn_%s" % action, ha_data) + self.speak_dialog('homeassistant.device.%s' % + action, data=ha_entity) + return + # TODO: need to figure out, if this indeed throws a KeyError + except KeyError: + self.log.debug("Not turn on/off all intent") + except: + self.log.debug( + "Unexpected error in turn all intent:", exc_info()[0]) + + # Hande single entity + + ha_entity = self._find_entity( + entity, + [ + 'group', + 'light', + 'fan', + 'switch', + 'scene', + 'input_boolean', + 'climate' + ] + ) + + # Exit if entiti not found or is unavailabe + if not ha_entity or not self._check_availability(ha_entity): + return + + self.log.debug("Entity State: %s" % ha_entity['state']) + + ha_data = {'entity_id': ha_entity['id']} + + # IDEA: set context for 'turn it off' again or similar + # self.set_context('Entity', ha_entity['dev_name']) + if ha_entity['state'] == action: + self.log.debug("Entity in requested state") + self.speak_dialog('homeassistant.device.already', data={ + "dev_name": ha_entity['dev_name'], 'action': action}) + elif action == "toggle": + self.ha.execute_service("homeassistant", "toggle", + ha_data) + if(ha_entity['state'] == 'off'): + action = 'on' + else: + action = 'off' + self.speak_dialog('homeassistant.device.%s' % action, + data=ha_entity) + elif action in ["on", "off"]: + self.speak_dialog('homeassistant.device.%s' % action, + data=ha_entity) + self.ha.execute_service("homeassistant", "turn_%s" % action, + ha_data) + else: + self.speak_dialog('homeassistant.error.sorry') + return + + def _handle_light_set(self, message): + entity = message.data["entity"] + try: + brightness_req = float(message.data["Brightnessvalue"]) + if brightness_req > 100 or brightness_req < 0: + self.speak_dialog('homeassistant.brightness.badreq') + except KeyError: + brightness_req = 10.0 + brightness_value = int(brightness_req / 100 * 255) + brightness_percentage = int(brightness_req) + self.log.debug("Entity: %s" % entity) + self.log.debug("Brightness Value: %s" % brightness_value) + self.log.debug("Brightness Percent: %s" % brightness_percentage) + + ha_entity = self._find_entity(entity, ['group', 'light']) + # Exit if entiti not found or is unavailabe + if not ha_entity or not self._check_availability(ha_entity): + return + + ha_data = {'entity_id': ha_entity['id']} + + # IDEA: set context for 'turn it off again' or similar + # self.set_context('Entity', ha_entity['dev_name']) + # Set values for HA + ha_data['brightness'] = brightness_value + self.ha.execute_service("light", "turn_on", ha_data) + # Set values for mycroft reply + ha_data['dev_name'] = ha_entity['dev_name'] + ha_data['brightness'] = brightness_req + self.speak_dialog('homeassistant.brightness.dimmed', + data=ha_data) + + return + + def _handle_shopping_list(self, message): + entity = message.data["Entity"] + ha_data = {'name': entity} + self.ha.execute_service("shopping_list", "add_item", ha_data) + self.speak_dialog("homeassistant.shopping.list") + return + + def _handle_light_adjust(self, message): + entity = message.data["Entity"] + action = message.data["Action"] + brightness_req = 10.0 + brightness_value = int(brightness_req / 100 * 255) + # brightness_percentage = int(brightness_req) # debating use + self.log.debug("Entity: %s" % entity) + self.log.debug("Brightness Value: %s" % brightness_value) + + # Set the min and max brightness for bulbs. Smart bulbs + # use 0-255 integer brightness, while spoken commands will + # use 0-100% brightness. + min_brightness = 5 + max_brightness = 255 + + ha_entity = self._find_entity(entity, ['group', 'light']) + # Exit if entiti not found or is unavailabe + if not ha_entity or not self._check_availability(ha_entity): + return + ha_data = {'entity_id': ha_entity['id']} + # IDEA: set context for 'turn it off again' or similar + # self.set_context('Entity', ha_entity['dev_name']) + + if action == "down": + if ha_entity['state'] == "off": + self.speak_dialog('homeassistant.brightness.cantdim.off', + data=ha_entity) + else: + light_attrs = self.ha.find_entity_attr(ha_entity['id']) + if light_attrs['unit_measure'] is None: + self.speak_dialog( + 'homeassistant.brightness.cantdim.dimmable', + data=ha_entity) + else: + ha_data['brightness'] = light_attrs['unit_measure'] - \ + brightness_value + if ha_data['brightness'] < min_brightness: + ha_data['brightness'] = min_brightness + self.ha.execute_service("homeassistant", + "turn_on", + ha_data) + ha_data['dev_name'] = ha_entity['dev_name'] + ha_data['brightness'] = round( + 100 / max_brightness * ha_data['brightness']) + self.speak_dialog('homeassistant.brightness.decreased', + data=ha_data) + elif action == "up": + if ha_entity['state'] == "off": + self.speak_dialog( + 'homeassistant.brightness.cantdim.off', + data=ha_entity) + else: + light_attrs = self.ha.find_entity_attr(ha_entity['id']) + if light_attrs['unit_measure'] is None: + self.speak_dialog( + 'homeassistant.brightness.cantdim.dimmable', + data=ha_entity) + else: + ha_data['brightness'] = light_attrs['unit_measure'] + \ + brightness_value + if ha_data['brightness'] > max_brightness: + ha_data['brightness'] = max_brightness + self.ha.execute_service("homeassistant", + "turn_on", + ha_data) + ha_data['dev_name'] = ha_entity['dev_name'] + ha_data['brightness'] = round( + 100 / max_brightness * ha_data['brightness']) + self.speak_dialog('homeassistant.brightness.increased', + data=ha_data) + else: + self.speak_dialog('homeassistant.error.sorry') + return + + def _handle_automation(self, message): + entity = message.data["Entity"] + self.log.debug("Entity: %s" % entity) + ha_entity = self._find_entity( + entity, + ['automation', 'scene', 'script'] + ) + + # Exit if entiti not found or is unavailabe + if not ha_entity or not self._check_availability(ha_entity): + return + + ha_data = {'entity_id': ha_entity['id']} + + # IDEA: set context for 'turn it off again' or similar + # self.set_context('Entity', ha_entity['dev_name']) + + self.log.debug("Triggered automation/scene/script: {}".format(ha_data)) + if "automation" in ha_entity['id']: + self.ha.execute_service('automation', 'trigger', ha_data) + self.speak_dialog('homeassistant.automation.trigger', + data={"dev_name": ha_entity['dev_name']}) + elif "script" in ha_entity['id']: + self.speak_dialog('homeassistant.automation.trigger', + data={"dev_name": ha_entity['dev_name']}) + self.ha.execute_service("script", "turn_on", + data=ha_data) + elif "scene" in ha_entity['id']: + self.speak_dialog('homeassistant.scene.on', + data=ha_entity) + self.ha.execute_service("scene", "turn_on", + data=ha_data) + + def _handle_sensor(self, message): + entity = message.data["Entity"] + self.log.debug("Entity: %s" % entity) + + ha_entity = self._find_entity(entity, ['sensor', 'switch']) + # Exit if entiti not found or is unavailabe + if not ha_entity or not self._check_availability(ha_entity): + return + + entity = ha_entity['id'] + + # IDEA: set context for 'read it out again' or similar + # self.set_context('Entity', ha_entity['dev_name']) + + unit_measurement = self.ha.find_entity_attr(entity) + sensor_unit = unit_measurement.get('unit_measure') or '' + + sensor_name = unit_measurement['name'] + sensor_state = unit_measurement['state'] + # extract unit for correct pronounciation + # this is fully optional + try: + from quantulum3 import parser + quantulumImport = True + except ImportError: + quantulumImport = False + + if quantulumImport and unit_measurement != '': + quantity = parser.parse((u'{} is {} {}'.format( + sensor_name, sensor_state, sensor_unit))) + if len(quantity) > 0: + quantity = quantity[0] + if (quantity.unit.name != "dimensionless" and + quantity.uncertainty <= 0.5): + sensor_unit = quantity.unit.name + sensor_state = quantity.value + + try: + value = float(sensor_state) + sensor_state = nice_number(value, lang=self.language) + except ValueError: + pass + + self.speak_dialog('homeassistant.sensor', data={ + "dev_name": sensor_name, + "value": sensor_state, + "unit": sensor_unit}) + # IDEA: Add some context if the person wants to look the unit up + # Maybe also change to name + # if one wants to look up "outside temperature" + # self.set_context("SubjectOfInterest", sensor_unit) + + # In progress, still testing. + # Device location works. + # Proximity might be an issue + # - overlapping command for directions modules + # - (e.g. "How far is x from y?") + def _handle_tracker(self, message): + entity = message.data["Entity"] + self.log.debug("Entity: %s" % entity) + + ha_entity = self._find_entity(entity, ['device_tracker']) + # Exit if entiti not found or is unavailabe + if not ha_entity or not self._check_availability(ha_entity): + return + + # IDEA: set context for 'locate it again' or similar + # self.set_context('Entity', ha_entity['dev_name']) + + entity = ha_entity['id'] + dev_name = ha_entity['dev_name'] + dev_location = ha_entity['state'] + self.speak_dialog('homeassistant.tracker.found', + data={'dev_name': dev_name, + 'location': dev_location}) + + def _handle_set_thermostat(self, message): + entity = message.data["entity"] + self.log.debug("Entity: %s" % entity) + self.log.debug("This is the message data: %s" % message.data) + temperature = message.data["temp"] + self.log.debug("Temperature: %s" % temperature) + + ha_entity = self._find_entity(entity, ['climate']) + # Exit if entiti not found or is unavailabe + if not ha_entity or not self._check_availability(ha_entity): + return + + climate_data = { + 'entity_id': ha_entity['id'], + 'temperature': temperature + } + climate_attr = self.ha.find_entity_attr(ha_entity['id']) + self.ha.execute_service("climate", "set_temperature", + data=climate_data) + self.speak_dialog('homeassistant.set.thermostat', + data={ + "dev_name": climate_attr['name'], + "value": temperature, + "unit": climate_attr['unit_measure']}) + + def handle_fallback(self, message): + if not self.enable_fallback: + return False + self._setup() + if self.ha is None: + self.speak_dialog('homeassistant.error.setup') + return False + # pass message to HA-server + response = self._handle_client_exception( + self.ha.engage_conversation, + message.data.get('utterance')) + if not response: + return False + # default non-parsing answer: "Sorry, I didn't understand that" + answer = response.get('speech') + if not answer or answer == "Sorry, I didn't understand that": + return False + + asked_question = False + # TODO: maybe enable conversation here if server asks sth like + # "In which room?" => answer should be directly passed to this skill + if answer.endswith("?"): + asked_question = True + self.speak(answer, expect_response=asked_question) + return True + + def shutdown(self): + self.remove_fallback(self.handle_fallback) + super(HomeAssistantSkill, self).shutdown() + + def stop(self): + pass + + +def create_skill(): + return HomeAssistantSkill() diff --git a/__init__.py b/__init__.py index 5a3cf8b6..80ceb3d9 100644 --- a/__init__.py +++ b/__init__.py @@ -225,13 +225,8 @@ def _handle_light_color(self, message): self.log.debug("Color: %s" % message.data["color"]) entity = message.data['entity'] - entity_parts = entity.split() voc_match = entity - # In case the utterance is for all entities - if "all" in entity_parts: - voc_match = "all lights" - if self.voc_match(voc_match, "all_lights"): data_obj = {'entity_id': 'all'} ha_entity = {'dev_name': 'all lights'} From a86d5c6d4baf76edb9722c990c0d1c42c81aeb11 Mon Sep 17 00:00:00 2001 From: Daniel Scicluna Date: Tue, 28 Sep 2021 11:43:03 +0200 Subject: [PATCH 12/13] remove history files --- .history/.gitignore_20210928112659 | 0 .history/.gitignore_20210928112806 | 14 - .history/__init___20210927201045.py | 636 --------------------------- .history/__init___20210927201253.py | 645 ---------------------------- .history/__init___20210928113153.py | 642 --------------------------- .history/__init___20210928113925.py | 637 --------------------------- 6 files changed, 2574 deletions(-) delete mode 100644 .history/.gitignore_20210928112659 delete mode 100644 .history/.gitignore_20210928112806 delete mode 100644 .history/__init___20210927201045.py delete mode 100644 .history/__init___20210927201253.py delete mode 100644 .history/__init___20210928113153.py delete mode 100644 .history/__init___20210928113925.py diff --git a/.history/.gitignore_20210928112659 b/.history/.gitignore_20210928112659 deleted file mode 100644 index e69de29b..00000000 diff --git a/.history/.gitignore_20210928112806 b/.history/.gitignore_20210928112806 deleted file mode 100644 index 137002bb..00000000 --- a/.history/.gitignore_20210928112806 +++ /dev/null @@ -1,14 +0,0 @@ -*.swp -*.pyc -.idea/misc.xml -.idea/modules.xml -.idea/mycroft-homeassistant.iml -.idea/workspace.xml -/.pydevproject -/.project -.idea/preferred-vcs.xml -/.idea/vcs.xml -/venv* -/test/integrationtests* -/settings.json -/skill_developers_testrunner.py diff --git a/.history/__init___20210927201045.py b/.history/__init___20210927201045.py deleted file mode 100644 index eb6c4448..00000000 --- a/.history/__init___20210927201045.py +++ /dev/null @@ -1,636 +0,0 @@ -from mycroft.skills.core import FallbackSkill -from mycroft.util.format import nice_number -from mycroft import MycroftSkill, intent_handler - -from sys import exc_info - -from requests.exceptions import ( - RequestException, - Timeout, - InvalidURL, - URLRequired, - SSLError, - HTTPError) -from requests.packages.urllib3.exceptions import MaxRetryError - -from .ha_client import HomeAssistantClient - - -__author__ = 'robconnolly, btotharye, nielstron' - -# Timeout time for HA requests -TIMEOUT = 10 - - -class HomeAssistantSkill(FallbackSkill): - - def __init__(self): - MycroftSkill.__init__(self) - super().__init__(name="HomeAssistantSkill") - self.ha = None - self.enable_fallback = False - - def _setup(self, force=False): - if self.settings is not None and (force or self.ha is None): - ip = self.settings.get('host') - token = self.settings.get('token') - - # Check if user filled IP, port and Token in configuration - if not ip: - self.speak_dialog('homeassistant.error.setup', data={ - "field": "I.P."}) - return - - if not token: - self.speak_dialog('homeassistant.error.setup', data={ - "field": "token"}) - return - - portnumber = self.settings.get('portnum') - try: - portnumber = int(portnumber) - except TypeError: - portnumber = 8123 - except ValueError: - # String might be some rubbish (like '') - self.speak_dialog('homeassistant.error.setup', data={ - "field": "port"}) - return - - self.ha = HomeAssistantClient( - ip, - token, - portnumber, - self.settings.get('ssl'), - self.settings.get('verify') - ) - if self.ha.connected(): - # Check if conversation component is loaded at HA-server - # and activate fallback accordingly (ha-server/api/components) - # TODO: enable other tools like dialogflow - conversation_activated = self.ha.find_component( - 'conversation' - ) - if conversation_activated: - self.enable_fallback = \ - self.settings.get('enable_fallback') - - def _force_setup(self): - self.log.debug('Creating a new HomeAssistant-Client') - self._setup(True) - - def initialize(self): - self.language = self.config_core.get('lang') - - # Needs higher priority than general fallback skills - self.register_fallback(self.handle_fallback, 2) - # Check and then monitor for credential changes - self.settings_change_callback = self.on_websettings_changed - self._setup() - - def on_websettings_changed(self): - # Force a setting refresh after the websettings changed - # Otherwise new settings will not be regarded - self._force_setup() - - # Try to find an entity on the HAServer - # Creates dialogs for errors and speaks them - # Returns None if nothing was found - # Else returns entity that was found - def _find_entity(self, entity, domains): - self._setup() - if self.ha is None: - self.speak_dialog('homeassistant.error.setup') - return False - # TODO if entity is 'all', 'any' or 'every' turn on - # every single entity not the whole group - ha_entity = self._handle_client_exception(self.ha.find_entity, - entity, domains) - if ha_entity is None: - self.speak_dialog('homeassistant.device.unknown', data={ - "dev_name": entity}) - return ha_entity - - # Routine for entiti availibility check - def _check_availability(self, ha_entity): - """ Simple routine for checking availability of entity inside - Home Assistent. """ - - if ha_entity['state'] == 'unavailable': - """ Check if state is `unavailable`, if yes, inform user about it. """ - - self.speak_dialog('homeassistant.device.unavailable', data={ - "dev_name": ha_entity['dev_name']}) - """ Return result to underliing function. """ - return False - return True - - # Calls passed method and catches often occurring exceptions - def _handle_client_exception(self, callback, *args, **kwargs): - try: - return callback(*args, **kwargs) - except Timeout: - self.speak_dialog('homeassistant.error.offline') - except (InvalidURL, URLRequired, MaxRetryError) as e: - if e.request is None or e.request.url is None: - # There is no url configured - self.speak_dialog('homeassistant.error.needurl') - else: - self.speak_dialog('homeassistant.error.invalidurl', data={ - 'url': e.request.url}) - except SSLError: - self.speak_dialog('homeassistant.error.ssl') - except HTTPError as e: - # check if due to wrong password - if e.response.status_code == 401: - self.speak_dialog('homeassistant.error.wrong_password') - else: - self.speak_dialog('homeassistant.error.http', data={ - 'code': e.response.status_code, - 'reason': e.response.reason}) - except (ConnectionError, RequestException) as exception: - # TODO find a nice member of any exception to output - self.speak_dialog('homeassistant.error', data={ - 'url': exception.request.url}) - - return False - - # Intent handlers - @intent_handler('turn.on.intent') - def handle_turn_on_intent(self, message): - self.log.debug("Turn on intent on entity: "+message.data.get("entity")) - message.data["Entity"] = message.data.get("entity") - message.data["Action"] = "on" - self._handle_turn_actions(message) - - @intent_handler('turn.off.intent') - def handle_turn_off_intent(self, message): - self.log.debug(message.data) - self.log.debug("Turn off intent on entity: "+message.data.get("entity")) - message.data["Entity"] = message.data.get("entity") - message.data["Action"] = "off" - self._handle_turn_actions(message) - - @intent_handler('toggle.intent') - def handle_toggle_intent(self, message): - self.log.debug("Toggle intent on entity: " + message.data.get("entity")) - message.data["Entity"] = message.data.get("entity") - message.data["Action"] = "toggle" - self._handle_turn_actions(message) - - @intent_handler('sensor.intent') - def handle_sensor_intent(self, message): - self.log.debug("Turn on intent on entity: "+message.data.get("entity")) - message.data["Entity"] = message.data.get("entity") - self._handle_sensor(message) - - @intent_handler('set.light.brightness.intent') - def handle_light_set_intent(self, message): - self.log.debug("Change light intensity: "+message.data.get("entity") \ - +"to"+message.data.get("brightnessvalue")+"percent") - message.data["Entity"] = message.data.get("entity") - message.data["Brightnessvalue"] = message.data.get("brightnessvalue") - self._handle_light_set(message) - - @intent_handler('increase.light.brightness.intent') - def handle_light_increase_intent(self, message): - self.log.debug("Increase light intensity: "+message.data.get("entity")) - message.data["Entity"] = message.data.get("entity") - message.data["Action"] = "up" - self._handle_light_adjust(message) - - @intent_handler('decrease.light.brightness.intent') - def handle_light_decrease_intent(self, message): - self.log.debug("Decrease light intensity: "+message.data.get("entity")) - message.data["Entity"] = message.data.get("entity") - message.data["Action"] = "down" - self._handle_light_adjust(message) - -<<<<<<< HEAD - @intent_handler('change.light.color.intent') - def handle_light_color_intent(self, message): - if not 'entity' in message.data: - self.speak_dialog('homeassistant.device.not.given', - data={"action": "change"}) - return - - self.log.debug("Change light colour intent on entity: " + - message.data['entity']) - self._handle_light_color(message) - - def _handle_light_color(self, message): - self.log.debug("Starting change colour intent.") - self.log.debug("Entity: %s" % message.data["entity"]) - self.log.debug("Color: %s" % message.data["color"]) - - entity = message.data['entity'] - entity_parts = entity.split() - voc_match = entity - - # In case the utterance is for all entities - if "all" in entity_parts: - voc_match = "all lights" - - if self.voc_match(voc_match, "all_lights"): - ha_data = {'entity_id': 'all'} - ha_entity = {'dev_name': 'all lights'} - else: - ha_entity = self._find_entity(entity, ['group', 'light']) - - if not ha_entity or not self._check_availability(ha_entity): - return - - ha_data = {'entity_id': ha_entity['id']} - - color = message.data['color'] - color_parts = list(color.split()) - - ha_data['color_name'] = message.data['color'] - self.ha.execute_service("light", "turn_on", ha_data) - - ha_data['dev_name'] = ha_entity['dev_name'] - self.speak_dialog('homeassistant.color.change', data=ha_data) -======= - @intent_handler('automation.intent') - def handle_automation_intent(self, message): - self.log.debug("Automation trigger intent on entity: "+message.data.get("entity")) - message.data["Entity"] = message.data.get("entity") - self._handle_automation(message) - - @intent_handler('tracker.intent') - def handle_tracker_intent(self, message): - self.log.debug("Turn on intent on entity: "+message.data.get("entity")) - message.data["Entity"] = message.data.get("entity") - self._handle_tracker(message) - - @intent_handler('set.climate.intent') - def handle_set_thermostat_intent(self, message): - self.log.debug("Set thermostat intent on entity: "+message.data.get("entity")) - message.data["Entity"] = message.data.get("entity") - message.data["Temp"] = message.data.get("temp") - self._handle_set_thermostat(message) - - @intent_handler('add.item.shopping.list.intent') - def handle_shopping_list_intent(self, message): - self.log.debug("Add : "+message.data.get("entity")+"to the shoping list") - message.data["Entity"] = message.data.get("entity") - self._handle_shopping_list(message) ->>>>>>> 2f8b7a452d22fe7abd9b53b89cb38af42c90837f - - def _handle_turn_actions(self, message): - self.log.debug("Starting Switch Intent") - entity = message.data["Entity"] - action = message.data["Action"] - self.log.debug("Entity: %s" % entity) - self.log.debug("Action: %s" % action) - - # Handle turn on/off all intent - try: - if self.voc_match(entity,"all_lights"): - domain = "light" - elif self.voc_match(entity,"all_switches"): - domain = "switch" - else: - domain = None - - if domain is not None: - ha_entity = {'dev_name': entity} - ha_data = {'entity_id': 'all'} - - self.ha.execute_service(domain, "turn_%s" % action, ha_data) - self.speak_dialog('homeassistant.device.%s' % action, data=ha_entity) - return - # TODO: need to figure out, if this indeed throws a KeyError - except KeyError: - self.log.debug("Not turn on/off all intent") - except: - self.log.debug("Unexpected error in turn all intent:", exc_info()[0]) - - # Hande single entity - - ha_entity = self._find_entity( - entity, - [ - 'group', - 'light', - 'fan', - 'switch', - 'scene', - 'input_boolean', - 'climate' - ] - ) - - # Exit if entiti not found or is unavailabe - if not ha_entity or not self._check_availability(ha_entity): - return - - self.log.debug("Entity State: %s" % ha_entity['state']) - - ha_data = {'entity_id': ha_entity['id']} - - # IDEA: set context for 'turn it off' again or similar - # self.set_context('Entity', ha_entity['dev_name']) - if ha_entity['state'] == action: - self.log.debug("Entity in requested state") - self.speak_dialog('homeassistant.device.already', data={ - "dev_name": ha_entity['dev_name'], 'action': action}) - elif action == "toggle": - self.ha.execute_service("homeassistant", "toggle", - ha_data) - if(ha_entity['state'] == 'off'): - action = 'on' - else: - action = 'off' - self.speak_dialog('homeassistant.device.%s' % action, - data=ha_entity) - elif action in ["on", "off"]: - self.speak_dialog('homeassistant.device.%s' % action, - data=ha_entity) - self.ha.execute_service("homeassistant", "turn_%s" % action, - ha_data) - else: - self.speak_dialog('homeassistant.error.sorry') - return - - def _handle_light_set(self, message): - entity = message.data["entity"] - try: - brightness_req = float(message.data["Brightnessvalue"]) - if brightness_req > 100 or brightness_req < 0: - self.speak_dialog('homeassistant.brightness.badreq') - except KeyError: - brightness_req = 10.0 - brightness_value = int(brightness_req / 100 * 255) - brightness_percentage = int(brightness_req) - self.log.debug("Entity: %s" % entity) - self.log.debug("Brightness Value: %s" % brightness_value) - self.log.debug("Brightness Percent: %s" % brightness_percentage) - - ha_entity = self._find_entity(entity, ['group', 'light']) - # Exit if entiti not found or is unavailabe - if not ha_entity or not self._check_availability(ha_entity): - return - - ha_data = {'entity_id': ha_entity['id']} - - # IDEA: set context for 'turn it off again' or similar - # self.set_context('Entity', ha_entity['dev_name']) - # Set values for HA - ha_data['brightness'] = brightness_value - self.ha.execute_service("light", "turn_on", ha_data) - # Set values for mycroft reply - ha_data['dev_name'] = ha_entity['dev_name'] - ha_data['brightness'] = brightness_req - self.speak_dialog('homeassistant.brightness.dimmed', - data=ha_data) - - return - - def _handle_shopping_list(self, message): - entity = message.data["Entity"] - ha_data = {'name': entity} - self.ha.execute_service("shopping_list", "add_item", ha_data) - self.speak_dialog("homeassistant.shopping.list") - return - - def _handle_light_adjust(self, message): - entity = message.data["Entity"] - action = message.data["Action"] - brightness_req = 10.0 - brightness_value = int(brightness_req / 100 * 255) - # brightness_percentage = int(brightness_req) # debating use - self.log.debug("Entity: %s" % entity) - self.log.debug("Brightness Value: %s" % brightness_value) - - # Set the min and max brightness for bulbs. Smart bulbs - # use 0-255 integer brightness, while spoken commands will - # use 0-100% brightness. - min_brightness = 5 - max_brightness = 255 - - ha_entity = self._find_entity(entity, ['group', 'light']) - # Exit if entiti not found or is unavailabe - if not ha_entity or not self._check_availability(ha_entity): - return - ha_data = {'entity_id': ha_entity['id']} - # IDEA: set context for 'turn it off again' or similar - # self.set_context('Entity', ha_entity['dev_name']) - - if action == "down": - if ha_entity['state'] == "off": - self.speak_dialog('homeassistant.brightness.cantdim.off', - data=ha_entity) - else: - light_attrs = self.ha.find_entity_attr(ha_entity['id']) - if light_attrs['unit_measure'] is None: - self.speak_dialog( - 'homeassistant.brightness.cantdim.dimmable', - data=ha_entity) - else: - ha_data['brightness'] = light_attrs['unit_measure'] - brightness_value - if ha_data['brightness'] < min_brightness: - ha_data['brightness'] = min_brightness - self.ha.execute_service("homeassistant", - "turn_on", - ha_data) - ha_data['dev_name'] = ha_entity['dev_name'] - ha_data['brightness'] = round(100 / max_brightness * ha_data['brightness']) - self.speak_dialog('homeassistant.brightness.decreased', - data=ha_data) - elif action == "up": - if ha_entity['state'] == "off": - self.speak_dialog( - 'homeassistant.brightness.cantdim.off', - data=ha_entity) - else: - light_attrs = self.ha.find_entity_attr(ha_entity['id']) - if light_attrs['unit_measure'] is None: - self.speak_dialog( - 'homeassistant.brightness.cantdim.dimmable', - data=ha_entity) - else: - ha_data['brightness'] = light_attrs['unit_measure'] + brightness_value - if ha_data['brightness'] > max_brightness: - ha_data['brightness'] = max_brightness - self.ha.execute_service("homeassistant", - "turn_on", - ha_data) - ha_data['dev_name'] = ha_entity['dev_name'] - ha_data['brightness'] = round(100 / max_brightness * ha_data['brightness']) - self.speak_dialog('homeassistant.brightness.increased', - data=ha_data) - else: - self.speak_dialog('homeassistant.error.sorry') - return - - def _handle_automation(self, message): - entity = message.data["Entity"] - self.log.debug("Entity: %s" % entity) - ha_entity = self._find_entity( - entity, - ['automation', 'scene', 'script'] - ) - - # Exit if entiti not found or is unavailabe - if not ha_entity or not self._check_availability(ha_entity): - return - - ha_data = {'entity_id': ha_entity['id']} - - # IDEA: set context for 'turn it off again' or similar - # self.set_context('Entity', ha_entity['dev_name']) - - self.log.debug("Triggered automation/scene/script: {}".format(ha_data)) - if "automation" in ha_entity['id']: - self.ha.execute_service('automation', 'trigger', ha_data) - self.speak_dialog('homeassistant.automation.trigger', - data={"dev_name": ha_entity['dev_name']}) - elif "script" in ha_entity['id']: - self.speak_dialog('homeassistant.automation.trigger', - data={"dev_name": ha_entity['dev_name']}) - self.ha.execute_service("script", "turn_on", - data=ha_data) - elif "scene" in ha_entity['id']: - self.speak_dialog('homeassistant.scene.on', - data=ha_entity) - self.ha.execute_service("scene", "turn_on", - data=ha_data) - - def _handle_sensor(self, message): - entity = message.data["Entity"] - self.log.debug("Entity: %s" % entity) - - ha_entity = self._find_entity(entity, ['sensor', 'switch']) - # Exit if entiti not found or is unavailabe - if not ha_entity or not self._check_availability(ha_entity): - return - - entity = ha_entity['id'] - - # IDEA: set context for 'read it out again' or similar - # self.set_context('Entity', ha_entity['dev_name']) - - unit_measurement = self.ha.find_entity_attr(entity) - sensor_unit = unit_measurement.get('unit_measure') or '' - - sensor_name = unit_measurement['name'] - sensor_state = unit_measurement['state'] - # extract unit for correct pronounciation - # this is fully optional - try: - from quantulum3 import parser - quantulumImport = True - except ImportError: - quantulumImport = False - - if quantulumImport and unit_measurement != '': - quantity = parser.parse((u'{} is {} {}'.format( - sensor_name, sensor_state, sensor_unit))) - if len(quantity) > 0: - quantity = quantity[0] - if (quantity.unit.name != "dimensionless" and - quantity.uncertainty <= 0.5): - sensor_unit = quantity.unit.name - sensor_state = quantity.value - - try: - value = float(sensor_state) - sensor_state = nice_number(value, lang=self.language) - except ValueError: - pass - - self.speak_dialog('homeassistant.sensor', data={ - "dev_name": sensor_name, - "value": sensor_state, - "unit": sensor_unit}) - # IDEA: Add some context if the person wants to look the unit up - # Maybe also change to name - # if one wants to look up "outside temperature" - # self.set_context("SubjectOfInterest", sensor_unit) - - # In progress, still testing. - # Device location works. - # Proximity might be an issue - # - overlapping command for directions modules - # - (e.g. "How far is x from y?") - def _handle_tracker(self, message): - entity = message.data["Entity"] - self.log.debug("Entity: %s" % entity) - - ha_entity = self._find_entity(entity, ['device_tracker']) - # Exit if entiti not found or is unavailabe - if not ha_entity or not self._check_availability(ha_entity): - return - - # IDEA: set context for 'locate it again' or similar - # self.set_context('Entity', ha_entity['dev_name']) - - entity = ha_entity['id'] - dev_name = ha_entity['dev_name'] - dev_location = ha_entity['state'] - self.speak_dialog('homeassistant.tracker.found', - data={'dev_name': dev_name, - 'location': dev_location}) - - def _handle_set_thermostat(self, message): - entity = message.data["entity"] - self.log.debug("Entity: %s" % entity) - self.log.debug("This is the message data: %s" % message.data) - temperature = message.data["temp"] - self.log.debug("Temperature: %s" % temperature) - - ha_entity = self._find_entity(entity, ['climate']) - # Exit if entiti not found or is unavailabe - if not ha_entity or not self._check_availability(ha_entity): - return - - climate_data = { - 'entity_id': ha_entity['id'], - 'temperature': temperature - } - climate_attr = self.ha.find_entity_attr(ha_entity['id']) - self.ha.execute_service("climate", "set_temperature", - data=climate_data) - self.speak_dialog('homeassistant.set.thermostat', - data={ - "dev_name": climate_attr['name'], - "value": temperature, - "unit": climate_attr['unit_measure']}) - - def handle_fallback(self, message): - if not self.enable_fallback: - return False - self._setup() - if self.ha is None: - self.speak_dialog('homeassistant.error.setup') - return False - # pass message to HA-server - response = self._handle_client_exception( - self.ha.engage_conversation, - message.data.get('utterance')) - if not response: - return False - # default non-parsing answer: "Sorry, I didn't understand that" - answer = response.get('speech') - if not answer or answer == "Sorry, I didn't understand that": - return False - - asked_question = False - # TODO: maybe enable conversation here if server asks sth like - # "In which room?" => answer should be directly passed to this skill - if answer.endswith("?"): - asked_question = True - self.speak(answer, expect_response=asked_question) - return True - - def shutdown(self): - self.remove_fallback(self.handle_fallback) - super(HomeAssistantSkill, self).shutdown() - - def stop(self): - pass - - -def create_skill(): - return HomeAssistantSkill() diff --git a/.history/__init___20210927201253.py b/.history/__init___20210927201253.py deleted file mode 100644 index d1db29c2..00000000 --- a/.history/__init___20210927201253.py +++ /dev/null @@ -1,645 +0,0 @@ -from mycroft.skills.core import FallbackSkill -from mycroft.util.format import nice_number -from mycroft import MycroftSkill, intent_handler - -from sys import exc_info - -from requests.exceptions import ( - RequestException, - Timeout, - InvalidURL, - URLRequired, - SSLError, - HTTPError) -from requests.packages.urllib3.exceptions import MaxRetryError - -from .ha_client import HomeAssistantClient - - -__author__ = 'robconnolly, btotharye, nielstron' - -# Timeout time for HA requests -TIMEOUT = 10 - - -class HomeAssistantSkill(FallbackSkill): - - def __init__(self): - MycroftSkill.__init__(self) - super().__init__(name="HomeAssistantSkill") - self.ha = None - self.enable_fallback = False - - def _setup(self, force=False): - if self.settings is not None and (force or self.ha is None): - ip = self.settings.get('host') - token = self.settings.get('token') - - # Check if user filled IP, port and Token in configuration - if not ip: - self.speak_dialog('homeassistant.error.setup', data={ - "field": "I.P."}) - return - - if not token: - self.speak_dialog('homeassistant.error.setup', data={ - "field": "token"}) - return - - portnumber = self.settings.get('portnum') - try: - portnumber = int(portnumber) - except TypeError: - portnumber = 8123 - except ValueError: - # String might be some rubbish (like '') - self.speak_dialog('homeassistant.error.setup', data={ - "field": "port"}) - return - - self.ha = HomeAssistantClient( - ip, - token, - portnumber, - self.settings.get('ssl'), - self.settings.get('verify') - ) - if self.ha.connected(): - # Check if conversation component is loaded at HA-server - # and activate fallback accordingly (ha-server/api/components) - # TODO: enable other tools like dialogflow - conversation_activated = self.ha.find_component( - 'conversation' - ) - if conversation_activated: - self.enable_fallback = \ - self.settings.get('enable_fallback') - - def _force_setup(self): - self.log.debug('Creating a new HomeAssistant-Client') - self._setup(True) - - def initialize(self): - self.language = self.config_core.get('lang') - - # Needs higher priority than general fallback skills - self.register_fallback(self.handle_fallback, 2) - # Check and then monitor for credential changes - self.settings_change_callback = self.on_websettings_changed - self._setup() - - def on_websettings_changed(self): - # Force a setting refresh after the websettings changed - # Otherwise new settings will not be regarded - self._force_setup() - - # Try to find an entity on the HAServer - # Creates dialogs for errors and speaks them - # Returns None if nothing was found - # Else returns entity that was found - def _find_entity(self, entity, domains): - self._setup() - if self.ha is None: - self.speak_dialog('homeassistant.error.setup') - return False - # TODO if entity is 'all', 'any' or 'every' turn on - # every single entity not the whole group - ha_entity = self._handle_client_exception(self.ha.find_entity, - entity, domains) - if ha_entity is None: - self.speak_dialog('homeassistant.device.unknown', data={ - "dev_name": entity}) - return ha_entity - - # Routine for entiti availibility check - def _check_availability(self, ha_entity): - """ Simple routine for checking availability of entity inside - Home Assistent. """ - - if ha_entity['state'] == 'unavailable': - """ Check if state is `unavailable`, if yes, inform user about it. """ - - self.speak_dialog('homeassistant.device.unavailable', data={ - "dev_name": ha_entity['dev_name']}) - """ Return result to underliing function. """ - return False - return True - - # Calls passed method and catches often occurring exceptions - def _handle_client_exception(self, callback, *args, **kwargs): - try: - return callback(*args, **kwargs) - except Timeout: - self.speak_dialog('homeassistant.error.offline') - except (InvalidURL, URLRequired, MaxRetryError) as e: - if e.request is None or e.request.url is None: - # There is no url configured - self.speak_dialog('homeassistant.error.needurl') - else: - self.speak_dialog('homeassistant.error.invalidurl', data={ - 'url': e.request.url}) - except SSLError: - self.speak_dialog('homeassistant.error.ssl') - except HTTPError as e: - # check if due to wrong password - if e.response.status_code == 401: - self.speak_dialog('homeassistant.error.wrong_password') - else: - self.speak_dialog('homeassistant.error.http', data={ - 'code': e.response.status_code, - 'reason': e.response.reason}) - except (ConnectionError, RequestException) as exception: - # TODO find a nice member of any exception to output - self.speak_dialog('homeassistant.error', data={ - 'url': exception.request.url}) - - return False - - # Intent handlers - @intent_handler('turn.on.intent') - def handle_turn_on_intent(self, message): - self.log.debug("Turn on intent on entity: "+message.data.get("entity")) - message.data["Entity"] = message.data.get("entity") - message.data["Action"] = "on" - self._handle_turn_actions(message) - - @intent_handler('turn.off.intent') - def handle_turn_off_intent(self, message): - self.log.debug(message.data) - self.log.debug("Turn off intent on entity: " + - message.data.get("entity")) - message.data["Entity"] = message.data.get("entity") - message.data["Action"] = "off" - self._handle_turn_actions(message) - - @intent_handler('toggle.intent') - def handle_toggle_intent(self, message): - self.log.debug("Toggle intent on entity: " + - message.data.get("entity")) - message.data["Entity"] = message.data.get("entity") - message.data["Action"] = "toggle" - self._handle_turn_actions(message) - - @intent_handler('sensor.intent') - def handle_sensor_intent(self, message): - self.log.debug("Turn on intent on entity: "+message.data.get("entity")) - message.data["Entity"] = message.data.get("entity") - self._handle_sensor(message) - - @intent_handler('set.light.brightness.intent') - def handle_light_set_intent(self, message): - self.log.debug("Change light intensity: "+message.data.get("entity") - + "to"+message.data.get("brightnessvalue")+"percent") - message.data["Entity"] = message.data.get("entity") - message.data["Brightnessvalue"] = message.data.get("brightnessvalue") - self._handle_light_set(message) - - @intent_handler('increase.light.brightness.intent') - def handle_light_increase_intent(self, message): - self.log.debug("Increase light intensity: "+message.data.get("entity")) - message.data["Entity"] = message.data.get("entity") - message.data["Action"] = "up" - self._handle_light_adjust(message) - - @intent_handler('decrease.light.brightness.intent') - def handle_light_decrease_intent(self, message): - self.log.debug("Decrease light intensity: "+message.data.get("entity")) - message.data["Entity"] = message.data.get("entity") - message.data["Action"] = "down" - self._handle_light_adjust(message) - - @intent_handler('change.light.color.intent') - def handle_light_color_intent(self, message): - if not 'entity' in message.data: - self.speak_dialog('homeassistant.device.not.given', - data={"action": "change"}) - return - - self.log.debug("Change light colour intent on entity: " + - message.data['entity']) - self._handle_light_color(message) - - def _handle_light_color(self, message): - self.log.debug("Starting change colour intent.") - self.log.debug("Entity: %s" % message.data["entity"]) - self.log.debug("Color: %s" % message.data["color"]) - - entity = message.data['entity'] - entity_parts = entity.split() - voc_match = entity - - # In case the utterance is for all entities - if "all" in entity_parts: - voc_match = "all lights" - - if self.voc_match(voc_match, "all_lights"): - ha_data = {'entity_id': 'all'} - ha_entity = {'dev_name': 'all lights'} - else: - ha_entity = self._find_entity(entity, ['group', 'light']) - - if not ha_entity or not self._check_availability(ha_entity): - return - - ha_data = {'entity_id': ha_entity['id']} - - color = message.data['color'] - color_parts = list(color.split()) - - ha_data['color_name'] = message.data['color'] - self.ha.execute_service("light", "turn_on", ha_data) - - ha_data['dev_name'] = ha_entity['dev_name'] - self.speak_dialog('homeassistant.color.change', data=ha_data) - - @intent_handler('automation.intent') - def handle_automation_intent(self, message): - self.log.debug("Automation trigger intent on entity: " + - message.data.get("entity")) - message.data["Entity"] = message.data.get("entity") - self._handle_automation(message) - - @intent_handler('tracker.intent') - def handle_tracker_intent(self, message): - self.log.debug("Turn on intent on entity: "+message.data.get("entity")) - message.data["Entity"] = message.data.get("entity") - self._handle_tracker(message) - - @intent_handler('set.climate.intent') - def handle_set_thermostat_intent(self, message): - self.log.debug("Set thermostat intent on entity: " + - message.data.get("entity")) - message.data["Entity"] = message.data.get("entity") - message.data["Temp"] = message.data.get("temp") - self._handle_set_thermostat(message) - - @intent_handler('add.item.shopping.list.intent') - def handle_shopping_list_intent(self, message): - self.log.debug("Add : "+message.data.get("entity") + - "to the shoping list") - message.data["Entity"] = message.data.get("entity") - self._handle_shopping_list(message) - - def _handle_turn_actions(self, message): - self.log.debug("Starting Switch Intent") - entity = message.data["Entity"] - action = message.data["Action"] - self.log.debug("Entity: %s" % entity) - self.log.debug("Action: %s" % action) - - # Handle turn on/off all intent - try: - if self.voc_match(entity, "all_lights"): - domain = "light" - elif self.voc_match(entity, "all_switches"): - domain = "switch" - else: - domain = None - - if domain is not None: - ha_entity = {'dev_name': entity} - ha_data = {'entity_id': 'all'} - - self.ha.execute_service(domain, "turn_%s" % action, ha_data) - self.speak_dialog('homeassistant.device.%s' % - action, data=ha_entity) - return - # TODO: need to figure out, if this indeed throws a KeyError - except KeyError: - self.log.debug("Not turn on/off all intent") - except: - self.log.debug( - "Unexpected error in turn all intent:", exc_info()[0]) - - # Hande single entity - - ha_entity = self._find_entity( - entity, - [ - 'group', - 'light', - 'fan', - 'switch', - 'scene', - 'input_boolean', - 'climate' - ] - ) - - # Exit if entiti not found or is unavailabe - if not ha_entity or not self._check_availability(ha_entity): - return - - self.log.debug("Entity State: %s" % ha_entity['state']) - - ha_data = {'entity_id': ha_entity['id']} - - # IDEA: set context for 'turn it off' again or similar - # self.set_context('Entity', ha_entity['dev_name']) - if ha_entity['state'] == action: - self.log.debug("Entity in requested state") - self.speak_dialog('homeassistant.device.already', data={ - "dev_name": ha_entity['dev_name'], 'action': action}) - elif action == "toggle": - self.ha.execute_service("homeassistant", "toggle", - ha_data) - if(ha_entity['state'] == 'off'): - action = 'on' - else: - action = 'off' - self.speak_dialog('homeassistant.device.%s' % action, - data=ha_entity) - elif action in ["on", "off"]: - self.speak_dialog('homeassistant.device.%s' % action, - data=ha_entity) - self.ha.execute_service("homeassistant", "turn_%s" % action, - ha_data) - else: - self.speak_dialog('homeassistant.error.sorry') - return - - def _handle_light_set(self, message): - entity = message.data["entity"] - try: - brightness_req = float(message.data["Brightnessvalue"]) - if brightness_req > 100 or brightness_req < 0: - self.speak_dialog('homeassistant.brightness.badreq') - except KeyError: - brightness_req = 10.0 - brightness_value = int(brightness_req / 100 * 255) - brightness_percentage = int(brightness_req) - self.log.debug("Entity: %s" % entity) - self.log.debug("Brightness Value: %s" % brightness_value) - self.log.debug("Brightness Percent: %s" % brightness_percentage) - - ha_entity = self._find_entity(entity, ['group', 'light']) - # Exit if entiti not found or is unavailabe - if not ha_entity or not self._check_availability(ha_entity): - return - - ha_data = {'entity_id': ha_entity['id']} - - # IDEA: set context for 'turn it off again' or similar - # self.set_context('Entity', ha_entity['dev_name']) - # Set values for HA - ha_data['brightness'] = brightness_value - self.ha.execute_service("light", "turn_on", ha_data) - # Set values for mycroft reply - ha_data['dev_name'] = ha_entity['dev_name'] - ha_data['brightness'] = brightness_req - self.speak_dialog('homeassistant.brightness.dimmed', - data=ha_data) - - return - - def _handle_shopping_list(self, message): - entity = message.data["Entity"] - ha_data = {'name': entity} - self.ha.execute_service("shopping_list", "add_item", ha_data) - self.speak_dialog("homeassistant.shopping.list") - return - - def _handle_light_adjust(self, message): - entity = message.data["Entity"] - action = message.data["Action"] - brightness_req = 10.0 - brightness_value = int(brightness_req / 100 * 255) - # brightness_percentage = int(brightness_req) # debating use - self.log.debug("Entity: %s" % entity) - self.log.debug("Brightness Value: %s" % brightness_value) - - # Set the min and max brightness for bulbs. Smart bulbs - # use 0-255 integer brightness, while spoken commands will - # use 0-100% brightness. - min_brightness = 5 - max_brightness = 255 - - ha_entity = self._find_entity(entity, ['group', 'light']) - # Exit if entiti not found or is unavailabe - if not ha_entity or not self._check_availability(ha_entity): - return - ha_data = {'entity_id': ha_entity['id']} - # IDEA: set context for 'turn it off again' or similar - # self.set_context('Entity', ha_entity['dev_name']) - - if action == "down": - if ha_entity['state'] == "off": - self.speak_dialog('homeassistant.brightness.cantdim.off', - data=ha_entity) - else: - light_attrs = self.ha.find_entity_attr(ha_entity['id']) - if light_attrs['unit_measure'] is None: - self.speak_dialog( - 'homeassistant.brightness.cantdim.dimmable', - data=ha_entity) - else: - ha_data['brightness'] = light_attrs['unit_measure'] - \ - brightness_value - if ha_data['brightness'] < min_brightness: - ha_data['brightness'] = min_brightness - self.ha.execute_service("homeassistant", - "turn_on", - ha_data) - ha_data['dev_name'] = ha_entity['dev_name'] - ha_data['brightness'] = round( - 100 / max_brightness * ha_data['brightness']) - self.speak_dialog('homeassistant.brightness.decreased', - data=ha_data) - elif action == "up": - if ha_entity['state'] == "off": - self.speak_dialog( - 'homeassistant.brightness.cantdim.off', - data=ha_entity) - else: - light_attrs = self.ha.find_entity_attr(ha_entity['id']) - if light_attrs['unit_measure'] is None: - self.speak_dialog( - 'homeassistant.brightness.cantdim.dimmable', - data=ha_entity) - else: - ha_data['brightness'] = light_attrs['unit_measure'] + \ - brightness_value - if ha_data['brightness'] > max_brightness: - ha_data['brightness'] = max_brightness - self.ha.execute_service("homeassistant", - "turn_on", - ha_data) - ha_data['dev_name'] = ha_entity['dev_name'] - ha_data['brightness'] = round( - 100 / max_brightness * ha_data['brightness']) - self.speak_dialog('homeassistant.brightness.increased', - data=ha_data) - else: - self.speak_dialog('homeassistant.error.sorry') - return - - def _handle_automation(self, message): - entity = message.data["Entity"] - self.log.debug("Entity: %s" % entity) - ha_entity = self._find_entity( - entity, - ['automation', 'scene', 'script'] - ) - - # Exit if entiti not found or is unavailabe - if not ha_entity or not self._check_availability(ha_entity): - return - - ha_data = {'entity_id': ha_entity['id']} - - # IDEA: set context for 'turn it off again' or similar - # self.set_context('Entity', ha_entity['dev_name']) - - self.log.debug("Triggered automation/scene/script: {}".format(ha_data)) - if "automation" in ha_entity['id']: - self.ha.execute_service('automation', 'trigger', ha_data) - self.speak_dialog('homeassistant.automation.trigger', - data={"dev_name": ha_entity['dev_name']}) - elif "script" in ha_entity['id']: - self.speak_dialog('homeassistant.automation.trigger', - data={"dev_name": ha_entity['dev_name']}) - self.ha.execute_service("script", "turn_on", - data=ha_data) - elif "scene" in ha_entity['id']: - self.speak_dialog('homeassistant.scene.on', - data=ha_entity) - self.ha.execute_service("scene", "turn_on", - data=ha_data) - - def _handle_sensor(self, message): - entity = message.data["Entity"] - self.log.debug("Entity: %s" % entity) - - ha_entity = self._find_entity(entity, ['sensor', 'switch']) - # Exit if entiti not found or is unavailabe - if not ha_entity or not self._check_availability(ha_entity): - return - - entity = ha_entity['id'] - - # IDEA: set context for 'read it out again' or similar - # self.set_context('Entity', ha_entity['dev_name']) - - unit_measurement = self.ha.find_entity_attr(entity) - sensor_unit = unit_measurement.get('unit_measure') or '' - - sensor_name = unit_measurement['name'] - sensor_state = unit_measurement['state'] - # extract unit for correct pronounciation - # this is fully optional - try: - from quantulum3 import parser - quantulumImport = True - except ImportError: - quantulumImport = False - - if quantulumImport and unit_measurement != '': - quantity = parser.parse((u'{} is {} {}'.format( - sensor_name, sensor_state, sensor_unit))) - if len(quantity) > 0: - quantity = quantity[0] - if (quantity.unit.name != "dimensionless" and - quantity.uncertainty <= 0.5): - sensor_unit = quantity.unit.name - sensor_state = quantity.value - - try: - value = float(sensor_state) - sensor_state = nice_number(value, lang=self.language) - except ValueError: - pass - - self.speak_dialog('homeassistant.sensor', data={ - "dev_name": sensor_name, - "value": sensor_state, - "unit": sensor_unit}) - # IDEA: Add some context if the person wants to look the unit up - # Maybe also change to name - # if one wants to look up "outside temperature" - # self.set_context("SubjectOfInterest", sensor_unit) - - # In progress, still testing. - # Device location works. - # Proximity might be an issue - # - overlapping command for directions modules - # - (e.g. "How far is x from y?") - def _handle_tracker(self, message): - entity = message.data["Entity"] - self.log.debug("Entity: %s" % entity) - - ha_entity = self._find_entity(entity, ['device_tracker']) - # Exit if entiti not found or is unavailabe - if not ha_entity or not self._check_availability(ha_entity): - return - - # IDEA: set context for 'locate it again' or similar - # self.set_context('Entity', ha_entity['dev_name']) - - entity = ha_entity['id'] - dev_name = ha_entity['dev_name'] - dev_location = ha_entity['state'] - self.speak_dialog('homeassistant.tracker.found', - data={'dev_name': dev_name, - 'location': dev_location}) - - def _handle_set_thermostat(self, message): - entity = message.data["entity"] - self.log.debug("Entity: %s" % entity) - self.log.debug("This is the message data: %s" % message.data) - temperature = message.data["temp"] - self.log.debug("Temperature: %s" % temperature) - - ha_entity = self._find_entity(entity, ['climate']) - # Exit if entiti not found or is unavailabe - if not ha_entity or not self._check_availability(ha_entity): - return - - climate_data = { - 'entity_id': ha_entity['id'], - 'temperature': temperature - } - climate_attr = self.ha.find_entity_attr(ha_entity['id']) - self.ha.execute_service("climate", "set_temperature", - data=climate_data) - self.speak_dialog('homeassistant.set.thermostat', - data={ - "dev_name": climate_attr['name'], - "value": temperature, - "unit": climate_attr['unit_measure']}) - - def handle_fallback(self, message): - if not self.enable_fallback: - return False - self._setup() - if self.ha is None: - self.speak_dialog('homeassistant.error.setup') - return False - # pass message to HA-server - response = self._handle_client_exception( - self.ha.engage_conversation, - message.data.get('utterance')) - if not response: - return False - # default non-parsing answer: "Sorry, I didn't understand that" - answer = response.get('speech') - if not answer or answer == "Sorry, I didn't understand that": - return False - - asked_question = False - # TODO: maybe enable conversation here if server asks sth like - # "In which room?" => answer should be directly passed to this skill - if answer.endswith("?"): - asked_question = True - self.speak(answer, expect_response=asked_question) - return True - - def shutdown(self): - self.remove_fallback(self.handle_fallback) - super(HomeAssistantSkill, self).shutdown() - - def stop(self): - pass - - -def create_skill(): - return HomeAssistantSkill() diff --git a/.history/__init___20210928113153.py b/.history/__init___20210928113153.py deleted file mode 100644 index 5a3cf8b6..00000000 --- a/.history/__init___20210928113153.py +++ /dev/null @@ -1,642 +0,0 @@ -from mycroft.skills.core import FallbackSkill -from mycroft.util.format import nice_number -from mycroft import MycroftSkill, intent_handler - -from sys import exc_info - -from requests.exceptions import ( - RequestException, - Timeout, - InvalidURL, - URLRequired, - SSLError, - HTTPError) -from requests.packages.urllib3.exceptions import MaxRetryError - -from .ha_client import HomeAssistantClient - - -__author__ = 'robconnolly, btotharye, nielstron' - -# Timeout time for HA requests -TIMEOUT = 10 - - -class HomeAssistantSkill(FallbackSkill): - - def __init__(self): - MycroftSkill.__init__(self) - super().__init__(name="HomeAssistantSkill") - self.ha = None - self.enable_fallback = False - - def _setup(self, force=False): - if self.settings is not None and (force or self.ha is None): - ip = self.settings.get('host') - token = self.settings.get('token') - - # Check if user filled IP, port and Token in configuration - if not ip: - self.speak_dialog('homeassistant.error.setup', data={ - "field": "I.P."}) - return - - if not token: - self.speak_dialog('homeassistant.error.setup', data={ - "field": "token"}) - return - - portnumber = self.settings.get('portnum') - try: - portnumber = int(portnumber) - except TypeError: - portnumber = 8123 - except ValueError: - # String might be some rubbish (like '') - self.speak_dialog('homeassistant.error.setup', data={ - "field": "port"}) - return - - self.ha = HomeAssistantClient( - ip, - token, - portnumber, - self.settings.get('ssl'), - self.settings.get('verify') - ) - if self.ha.connected(): - # Check if conversation component is loaded at HA-server - # and activate fallback accordingly (ha-server/api/components) - # TODO: enable other tools like dialogflow - conversation_activated = self.ha.find_component( - 'conversation' - ) - if conversation_activated: - self.enable_fallback = \ - self.settings.get('enable_fallback') - - def _force_setup(self): - self.log.debug('Creating a new HomeAssistant-Client') - self._setup(True) - - def initialize(self): - self.language = self.config_core.get('lang') - - # Needs higher priority than general fallback skills - self.register_fallback(self.handle_fallback, 2) - # Check and then monitor for credential changes - self.settings_change_callback = self.on_websettings_changed - self._setup() - - def on_websettings_changed(self): - # Force a setting refresh after the websettings changed - # Otherwise new settings will not be regarded - self._force_setup() - - # Try to find an entity on the HAServer - # Creates dialogs for errors and speaks them - # Returns None if nothing was found - # Else returns entity that was found - def _find_entity(self, entity, domains): - self._setup() - if self.ha is None: - self.speak_dialog('homeassistant.error.setup') - return False - # TODO if entity is 'all', 'any' or 'every' turn on - # every single entity not the whole group - ha_entity = self._handle_client_exception(self.ha.find_entity, - entity, domains) - if ha_entity is None: - self.speak_dialog('homeassistant.device.unknown', data={ - "dev_name": entity}) - return ha_entity - - # Routine for entiti availibility check - def _check_availability(self, ha_entity): - """ Simple routine for checking availability of entity inside - Home Assistent. """ - - if ha_entity['state'] == 'unavailable': - """ Check if state is `unavailable`, if yes, inform user about it. """ - - self.speak_dialog('homeassistant.device.unavailable', data={ - "dev_name": ha_entity['dev_name']}) - """ Return result to underliing function. """ - return False - return True - - # Calls passed method and catches often occurring exceptions - def _handle_client_exception(self, callback, *args, **kwargs): - try: - return callback(*args, **kwargs) - except Timeout: - self.speak_dialog('homeassistant.error.offline') - except (InvalidURL, URLRequired, MaxRetryError) as e: - if e.request is None or e.request.url is None: - # There is no url configured - self.speak_dialog('homeassistant.error.needurl') - else: - self.speak_dialog('homeassistant.error.invalidurl', data={ - 'url': e.request.url}) - except SSLError: - self.speak_dialog('homeassistant.error.ssl') - except HTTPError as e: - # check if due to wrong password - if e.response.status_code == 401: - self.speak_dialog('homeassistant.error.wrong_password') - else: - self.speak_dialog('homeassistant.error.http', data={ - 'code': e.response.status_code, - 'reason': e.response.reason}) - except (ConnectionError, RequestException) as exception: - # TODO find a nice member of any exception to output - self.speak_dialog('homeassistant.error', data={ - 'url': exception.request.url}) - - return False - - # Intent handlers - @intent_handler('turn.on.intent') - def handle_turn_on_intent(self, message): - self.log.debug("Turn on intent on entity: "+message.data.get("entity")) - message.data["Entity"] = message.data.get("entity") - message.data["Action"] = "on" - self._handle_turn_actions(message) - - @intent_handler('turn.off.intent') - def handle_turn_off_intent(self, message): - self.log.debug(message.data) - self.log.debug("Turn off intent on entity: " + - message.data.get("entity")) - message.data["Entity"] = message.data.get("entity") - message.data["Action"] = "off" - self._handle_turn_actions(message) - - @intent_handler('toggle.intent') - def handle_toggle_intent(self, message): - self.log.debug("Toggle intent on entity: " + - message.data.get("entity")) - message.data["Entity"] = message.data.get("entity") - message.data["Action"] = "toggle" - self._handle_turn_actions(message) - - @intent_handler('sensor.intent') - def handle_sensor_intent(self, message): - self.log.debug("Turn on intent on entity: "+message.data.get("entity")) - message.data["Entity"] = message.data.get("entity") - self._handle_sensor(message) - - @intent_handler('set.light.brightness.intent') - def handle_light_set_intent(self, message): - self.log.debug("Change light intensity: "+message.data.get("entity") - + "to"+message.data.get("brightnessvalue")+"percent") - message.data["Entity"] = message.data.get("entity") - message.data["Brightnessvalue"] = message.data.get("brightnessvalue") - self._handle_light_set(message) - - @intent_handler('increase.light.brightness.intent') - def handle_light_increase_intent(self, message): - self.log.debug("Increase light intensity: "+message.data.get("entity")) - message.data["Entity"] = message.data.get("entity") - message.data["Action"] = "up" - self._handle_light_adjust(message) - - @intent_handler('decrease.light.brightness.intent') - def handle_light_decrease_intent(self, message): - self.log.debug("Decrease light intensity: "+message.data.get("entity")) - message.data["Entity"] = message.data.get("entity") - message.data["Action"] = "down" - self._handle_light_adjust(message) - - @intent_handler('change.light.color.intent') - def handle_light_color_intent(self, message): - if not 'entity' in message.data: - self.speak_dialog('homeassistant.device.not.given', - data={"action": "change"}) - return - - self.log.debug("Change light colour intent on entity: " + - message.data['entity']) - self._handle_light_color(message) - - def _handle_light_color(self, message): - self.log.debug("Starting change colour intent.") - self.log.debug("Entity: %s" % message.data["entity"]) - self.log.debug("Color: %s" % message.data["color"]) - - entity = message.data['entity'] - entity_parts = entity.split() - voc_match = entity - - # In case the utterance is for all entities - if "all" in entity_parts: - voc_match = "all lights" - - if self.voc_match(voc_match, "all_lights"): - data_obj = {'entity_id': 'all'} - ha_entity = {'dev_name': 'all lights'} - else: - ha_entity = self._find_entity(entity, ['group', 'light']) - - if not ha_entity or not self._check_availability(ha_entity): - return - - data_obj = {'entity_id': ha_entity['id']} - - data_obj['color_name'] = message.data['color'] - self.ha.execute_service("light", "turn_on", data_obj) - - data_obj['dev_name'] = ha_entity['dev_name'] - self.speak_dialog('homeassistant.color.change', data=data_obj) - - @intent_handler('automation.intent') - def handle_automation_intent(self, message): - self.log.debug("Automation trigger intent on entity: " + - message.data.get("entity")) - message.data["Entity"] = message.data.get("entity") - self._handle_automation(message) - - @intent_handler('tracker.intent') - def handle_tracker_intent(self, message): - self.log.debug("Turn on intent on entity: "+message.data.get("entity")) - message.data["Entity"] = message.data.get("entity") - self._handle_tracker(message) - - @intent_handler('set.climate.intent') - def handle_set_thermostat_intent(self, message): - self.log.debug("Set thermostat intent on entity: " + - message.data.get("entity")) - message.data["Entity"] = message.data.get("entity") - message.data["Temp"] = message.data.get("temp") - self._handle_set_thermostat(message) - - @intent_handler('add.item.shopping.list.intent') - def handle_shopping_list_intent(self, message): - self.log.debug("Add : "+message.data.get("entity") + - "to the shoping list") - message.data["Entity"] = message.data.get("entity") - self._handle_shopping_list(message) - - def _handle_turn_actions(self, message): - self.log.debug("Starting Switch Intent") - entity = message.data["Entity"] - action = message.data["Action"] - self.log.debug("Entity: %s" % entity) - self.log.debug("Action: %s" % action) - - # Handle turn on/off all intent - try: - if self.voc_match(entity, "all_lights"): - domain = "light" - elif self.voc_match(entity, "all_switches"): - domain = "switch" - else: - domain = None - - if domain is not None: - ha_entity = {'dev_name': entity} - ha_data = {'entity_id': 'all'} - - self.ha.execute_service(domain, "turn_%s" % action, ha_data) - self.speak_dialog('homeassistant.device.%s' % - action, data=ha_entity) - return - # TODO: need to figure out, if this indeed throws a KeyError - except KeyError: - self.log.debug("Not turn on/off all intent") - except: - self.log.debug( - "Unexpected error in turn all intent:", exc_info()[0]) - - # Hande single entity - - ha_entity = self._find_entity( - entity, - [ - 'group', - 'light', - 'fan', - 'switch', - 'scene', - 'input_boolean', - 'climate' - ] - ) - - # Exit if entiti not found or is unavailabe - if not ha_entity or not self._check_availability(ha_entity): - return - - self.log.debug("Entity State: %s" % ha_entity['state']) - - ha_data = {'entity_id': ha_entity['id']} - - # IDEA: set context for 'turn it off' again or similar - # self.set_context('Entity', ha_entity['dev_name']) - if ha_entity['state'] == action: - self.log.debug("Entity in requested state") - self.speak_dialog('homeassistant.device.already', data={ - "dev_name": ha_entity['dev_name'], 'action': action}) - elif action == "toggle": - self.ha.execute_service("homeassistant", "toggle", - ha_data) - if(ha_entity['state'] == 'off'): - action = 'on' - else: - action = 'off' - self.speak_dialog('homeassistant.device.%s' % action, - data=ha_entity) - elif action in ["on", "off"]: - self.speak_dialog('homeassistant.device.%s' % action, - data=ha_entity) - self.ha.execute_service("homeassistant", "turn_%s" % action, - ha_data) - else: - self.speak_dialog('homeassistant.error.sorry') - return - - def _handle_light_set(self, message): - entity = message.data["entity"] - try: - brightness_req = float(message.data["Brightnessvalue"]) - if brightness_req > 100 or brightness_req < 0: - self.speak_dialog('homeassistant.brightness.badreq') - except KeyError: - brightness_req = 10.0 - brightness_value = int(brightness_req / 100 * 255) - brightness_percentage = int(brightness_req) - self.log.debug("Entity: %s" % entity) - self.log.debug("Brightness Value: %s" % brightness_value) - self.log.debug("Brightness Percent: %s" % brightness_percentage) - - ha_entity = self._find_entity(entity, ['group', 'light']) - # Exit if entiti not found or is unavailabe - if not ha_entity or not self._check_availability(ha_entity): - return - - ha_data = {'entity_id': ha_entity['id']} - - # IDEA: set context for 'turn it off again' or similar - # self.set_context('Entity', ha_entity['dev_name']) - # Set values for HA - ha_data['brightness'] = brightness_value - self.ha.execute_service("light", "turn_on", ha_data) - # Set values for mycroft reply - ha_data['dev_name'] = ha_entity['dev_name'] - ha_data['brightness'] = brightness_req - self.speak_dialog('homeassistant.brightness.dimmed', - data=ha_data) - - return - - def _handle_shopping_list(self, message): - entity = message.data["Entity"] - ha_data = {'name': entity} - self.ha.execute_service("shopping_list", "add_item", ha_data) - self.speak_dialog("homeassistant.shopping.list") - return - - def _handle_light_adjust(self, message): - entity = message.data["Entity"] - action = message.data["Action"] - brightness_req = 10.0 - brightness_value = int(brightness_req / 100 * 255) - # brightness_percentage = int(brightness_req) # debating use - self.log.debug("Entity: %s" % entity) - self.log.debug("Brightness Value: %s" % brightness_value) - - # Set the min and max brightness for bulbs. Smart bulbs - # use 0-255 integer brightness, while spoken commands will - # use 0-100% brightness. - min_brightness = 5 - max_brightness = 255 - - ha_entity = self._find_entity(entity, ['group', 'light']) - # Exit if entiti not found or is unavailabe - if not ha_entity or not self._check_availability(ha_entity): - return - ha_data = {'entity_id': ha_entity['id']} - # IDEA: set context for 'turn it off again' or similar - # self.set_context('Entity', ha_entity['dev_name']) - - if action == "down": - if ha_entity['state'] == "off": - self.speak_dialog('homeassistant.brightness.cantdim.off', - data=ha_entity) - else: - light_attrs = self.ha.find_entity_attr(ha_entity['id']) - if light_attrs['unit_measure'] is None: - self.speak_dialog( - 'homeassistant.brightness.cantdim.dimmable', - data=ha_entity) - else: - ha_data['brightness'] = light_attrs['unit_measure'] - \ - brightness_value - if ha_data['brightness'] < min_brightness: - ha_data['brightness'] = min_brightness - self.ha.execute_service("homeassistant", - "turn_on", - ha_data) - ha_data['dev_name'] = ha_entity['dev_name'] - ha_data['brightness'] = round( - 100 / max_brightness * ha_data['brightness']) - self.speak_dialog('homeassistant.brightness.decreased', - data=ha_data) - elif action == "up": - if ha_entity['state'] == "off": - self.speak_dialog( - 'homeassistant.brightness.cantdim.off', - data=ha_entity) - else: - light_attrs = self.ha.find_entity_attr(ha_entity['id']) - if light_attrs['unit_measure'] is None: - self.speak_dialog( - 'homeassistant.brightness.cantdim.dimmable', - data=ha_entity) - else: - ha_data['brightness'] = light_attrs['unit_measure'] + \ - brightness_value - if ha_data['brightness'] > max_brightness: - ha_data['brightness'] = max_brightness - self.ha.execute_service("homeassistant", - "turn_on", - ha_data) - ha_data['dev_name'] = ha_entity['dev_name'] - ha_data['brightness'] = round( - 100 / max_brightness * ha_data['brightness']) - self.speak_dialog('homeassistant.brightness.increased', - data=ha_data) - else: - self.speak_dialog('homeassistant.error.sorry') - return - - def _handle_automation(self, message): - entity = message.data["Entity"] - self.log.debug("Entity: %s" % entity) - ha_entity = self._find_entity( - entity, - ['automation', 'scene', 'script'] - ) - - # Exit if entiti not found or is unavailabe - if not ha_entity or not self._check_availability(ha_entity): - return - - ha_data = {'entity_id': ha_entity['id']} - - # IDEA: set context for 'turn it off again' or similar - # self.set_context('Entity', ha_entity['dev_name']) - - self.log.debug("Triggered automation/scene/script: {}".format(ha_data)) - if "automation" in ha_entity['id']: - self.ha.execute_service('automation', 'trigger', ha_data) - self.speak_dialog('homeassistant.automation.trigger', - data={"dev_name": ha_entity['dev_name']}) - elif "script" in ha_entity['id']: - self.speak_dialog('homeassistant.automation.trigger', - data={"dev_name": ha_entity['dev_name']}) - self.ha.execute_service("script", "turn_on", - data=ha_data) - elif "scene" in ha_entity['id']: - self.speak_dialog('homeassistant.scene.on', - data=ha_entity) - self.ha.execute_service("scene", "turn_on", - data=ha_data) - - def _handle_sensor(self, message): - entity = message.data["Entity"] - self.log.debug("Entity: %s" % entity) - - ha_entity = self._find_entity(entity, ['sensor', 'switch']) - # Exit if entiti not found or is unavailabe - if not ha_entity or not self._check_availability(ha_entity): - return - - entity = ha_entity['id'] - - # IDEA: set context for 'read it out again' or similar - # self.set_context('Entity', ha_entity['dev_name']) - - unit_measurement = self.ha.find_entity_attr(entity) - sensor_unit = unit_measurement.get('unit_measure') or '' - - sensor_name = unit_measurement['name'] - sensor_state = unit_measurement['state'] - # extract unit for correct pronounciation - # this is fully optional - try: - from quantulum3 import parser - quantulumImport = True - except ImportError: - quantulumImport = False - - if quantulumImport and unit_measurement != '': - quantity = parser.parse((u'{} is {} {}'.format( - sensor_name, sensor_state, sensor_unit))) - if len(quantity) > 0: - quantity = quantity[0] - if (quantity.unit.name != "dimensionless" and - quantity.uncertainty <= 0.5): - sensor_unit = quantity.unit.name - sensor_state = quantity.value - - try: - value = float(sensor_state) - sensor_state = nice_number(value, lang=self.language) - except ValueError: - pass - - self.speak_dialog('homeassistant.sensor', data={ - "dev_name": sensor_name, - "value": sensor_state, - "unit": sensor_unit}) - # IDEA: Add some context if the person wants to look the unit up - # Maybe also change to name - # if one wants to look up "outside temperature" - # self.set_context("SubjectOfInterest", sensor_unit) - - # In progress, still testing. - # Device location works. - # Proximity might be an issue - # - overlapping command for directions modules - # - (e.g. "How far is x from y?") - def _handle_tracker(self, message): - entity = message.data["Entity"] - self.log.debug("Entity: %s" % entity) - - ha_entity = self._find_entity(entity, ['device_tracker']) - # Exit if entiti not found or is unavailabe - if not ha_entity or not self._check_availability(ha_entity): - return - - # IDEA: set context for 'locate it again' or similar - # self.set_context('Entity', ha_entity['dev_name']) - - entity = ha_entity['id'] - dev_name = ha_entity['dev_name'] - dev_location = ha_entity['state'] - self.speak_dialog('homeassistant.tracker.found', - data={'dev_name': dev_name, - 'location': dev_location}) - - def _handle_set_thermostat(self, message): - entity = message.data["entity"] - self.log.debug("Entity: %s" % entity) - self.log.debug("This is the message data: %s" % message.data) - temperature = message.data["temp"] - self.log.debug("Temperature: %s" % temperature) - - ha_entity = self._find_entity(entity, ['climate']) - # Exit if entiti not found or is unavailabe - if not ha_entity or not self._check_availability(ha_entity): - return - - climate_data = { - 'entity_id': ha_entity['id'], - 'temperature': temperature - } - climate_attr = self.ha.find_entity_attr(ha_entity['id']) - self.ha.execute_service("climate", "set_temperature", - data=climate_data) - self.speak_dialog('homeassistant.set.thermostat', - data={ - "dev_name": climate_attr['name'], - "value": temperature, - "unit": climate_attr['unit_measure']}) - - def handle_fallback(self, message): - if not self.enable_fallback: - return False - self._setup() - if self.ha is None: - self.speak_dialog('homeassistant.error.setup') - return False - # pass message to HA-server - response = self._handle_client_exception( - self.ha.engage_conversation, - message.data.get('utterance')) - if not response: - return False - # default non-parsing answer: "Sorry, I didn't understand that" - answer = response.get('speech') - if not answer or answer == "Sorry, I didn't understand that": - return False - - asked_question = False - # TODO: maybe enable conversation here if server asks sth like - # "In which room?" => answer should be directly passed to this skill - if answer.endswith("?"): - asked_question = True - self.speak(answer, expect_response=asked_question) - return True - - def shutdown(self): - self.remove_fallback(self.handle_fallback) - super(HomeAssistantSkill, self).shutdown() - - def stop(self): - pass - - -def create_skill(): - return HomeAssistantSkill() diff --git a/.history/__init___20210928113925.py b/.history/__init___20210928113925.py deleted file mode 100644 index 80ceb3d9..00000000 --- a/.history/__init___20210928113925.py +++ /dev/null @@ -1,637 +0,0 @@ -from mycroft.skills.core import FallbackSkill -from mycroft.util.format import nice_number -from mycroft import MycroftSkill, intent_handler - -from sys import exc_info - -from requests.exceptions import ( - RequestException, - Timeout, - InvalidURL, - URLRequired, - SSLError, - HTTPError) -from requests.packages.urllib3.exceptions import MaxRetryError - -from .ha_client import HomeAssistantClient - - -__author__ = 'robconnolly, btotharye, nielstron' - -# Timeout time for HA requests -TIMEOUT = 10 - - -class HomeAssistantSkill(FallbackSkill): - - def __init__(self): - MycroftSkill.__init__(self) - super().__init__(name="HomeAssistantSkill") - self.ha = None - self.enable_fallback = False - - def _setup(self, force=False): - if self.settings is not None and (force or self.ha is None): - ip = self.settings.get('host') - token = self.settings.get('token') - - # Check if user filled IP, port and Token in configuration - if not ip: - self.speak_dialog('homeassistant.error.setup', data={ - "field": "I.P."}) - return - - if not token: - self.speak_dialog('homeassistant.error.setup', data={ - "field": "token"}) - return - - portnumber = self.settings.get('portnum') - try: - portnumber = int(portnumber) - except TypeError: - portnumber = 8123 - except ValueError: - # String might be some rubbish (like '') - self.speak_dialog('homeassistant.error.setup', data={ - "field": "port"}) - return - - self.ha = HomeAssistantClient( - ip, - token, - portnumber, - self.settings.get('ssl'), - self.settings.get('verify') - ) - if self.ha.connected(): - # Check if conversation component is loaded at HA-server - # and activate fallback accordingly (ha-server/api/components) - # TODO: enable other tools like dialogflow - conversation_activated = self.ha.find_component( - 'conversation' - ) - if conversation_activated: - self.enable_fallback = \ - self.settings.get('enable_fallback') - - def _force_setup(self): - self.log.debug('Creating a new HomeAssistant-Client') - self._setup(True) - - def initialize(self): - self.language = self.config_core.get('lang') - - # Needs higher priority than general fallback skills - self.register_fallback(self.handle_fallback, 2) - # Check and then monitor for credential changes - self.settings_change_callback = self.on_websettings_changed - self._setup() - - def on_websettings_changed(self): - # Force a setting refresh after the websettings changed - # Otherwise new settings will not be regarded - self._force_setup() - - # Try to find an entity on the HAServer - # Creates dialogs for errors and speaks them - # Returns None if nothing was found - # Else returns entity that was found - def _find_entity(self, entity, domains): - self._setup() - if self.ha is None: - self.speak_dialog('homeassistant.error.setup') - return False - # TODO if entity is 'all', 'any' or 'every' turn on - # every single entity not the whole group - ha_entity = self._handle_client_exception(self.ha.find_entity, - entity, domains) - if ha_entity is None: - self.speak_dialog('homeassistant.device.unknown', data={ - "dev_name": entity}) - return ha_entity - - # Routine for entiti availibility check - def _check_availability(self, ha_entity): - """ Simple routine for checking availability of entity inside - Home Assistent. """ - - if ha_entity['state'] == 'unavailable': - """ Check if state is `unavailable`, if yes, inform user about it. """ - - self.speak_dialog('homeassistant.device.unavailable', data={ - "dev_name": ha_entity['dev_name']}) - """ Return result to underliing function. """ - return False - return True - - # Calls passed method and catches often occurring exceptions - def _handle_client_exception(self, callback, *args, **kwargs): - try: - return callback(*args, **kwargs) - except Timeout: - self.speak_dialog('homeassistant.error.offline') - except (InvalidURL, URLRequired, MaxRetryError) as e: - if e.request is None or e.request.url is None: - # There is no url configured - self.speak_dialog('homeassistant.error.needurl') - else: - self.speak_dialog('homeassistant.error.invalidurl', data={ - 'url': e.request.url}) - except SSLError: - self.speak_dialog('homeassistant.error.ssl') - except HTTPError as e: - # check if due to wrong password - if e.response.status_code == 401: - self.speak_dialog('homeassistant.error.wrong_password') - else: - self.speak_dialog('homeassistant.error.http', data={ - 'code': e.response.status_code, - 'reason': e.response.reason}) - except (ConnectionError, RequestException) as exception: - # TODO find a nice member of any exception to output - self.speak_dialog('homeassistant.error', data={ - 'url': exception.request.url}) - - return False - - # Intent handlers - @intent_handler('turn.on.intent') - def handle_turn_on_intent(self, message): - self.log.debug("Turn on intent on entity: "+message.data.get("entity")) - message.data["Entity"] = message.data.get("entity") - message.data["Action"] = "on" - self._handle_turn_actions(message) - - @intent_handler('turn.off.intent') - def handle_turn_off_intent(self, message): - self.log.debug(message.data) - self.log.debug("Turn off intent on entity: " + - message.data.get("entity")) - message.data["Entity"] = message.data.get("entity") - message.data["Action"] = "off" - self._handle_turn_actions(message) - - @intent_handler('toggle.intent') - def handle_toggle_intent(self, message): - self.log.debug("Toggle intent on entity: " + - message.data.get("entity")) - message.data["Entity"] = message.data.get("entity") - message.data["Action"] = "toggle" - self._handle_turn_actions(message) - - @intent_handler('sensor.intent') - def handle_sensor_intent(self, message): - self.log.debug("Turn on intent on entity: "+message.data.get("entity")) - message.data["Entity"] = message.data.get("entity") - self._handle_sensor(message) - - @intent_handler('set.light.brightness.intent') - def handle_light_set_intent(self, message): - self.log.debug("Change light intensity: "+message.data.get("entity") - + "to"+message.data.get("brightnessvalue")+"percent") - message.data["Entity"] = message.data.get("entity") - message.data["Brightnessvalue"] = message.data.get("brightnessvalue") - self._handle_light_set(message) - - @intent_handler('increase.light.brightness.intent') - def handle_light_increase_intent(self, message): - self.log.debug("Increase light intensity: "+message.data.get("entity")) - message.data["Entity"] = message.data.get("entity") - message.data["Action"] = "up" - self._handle_light_adjust(message) - - @intent_handler('decrease.light.brightness.intent') - def handle_light_decrease_intent(self, message): - self.log.debug("Decrease light intensity: "+message.data.get("entity")) - message.data["Entity"] = message.data.get("entity") - message.data["Action"] = "down" - self._handle_light_adjust(message) - - @intent_handler('change.light.color.intent') - def handle_light_color_intent(self, message): - if not 'entity' in message.data: - self.speak_dialog('homeassistant.device.not.given', - data={"action": "change"}) - return - - self.log.debug("Change light colour intent on entity: " + - message.data['entity']) - self._handle_light_color(message) - - def _handle_light_color(self, message): - self.log.debug("Starting change colour intent.") - self.log.debug("Entity: %s" % message.data["entity"]) - self.log.debug("Color: %s" % message.data["color"]) - - entity = message.data['entity'] - voc_match = entity - - if self.voc_match(voc_match, "all_lights"): - data_obj = {'entity_id': 'all'} - ha_entity = {'dev_name': 'all lights'} - else: - ha_entity = self._find_entity(entity, ['group', 'light']) - - if not ha_entity or not self._check_availability(ha_entity): - return - - data_obj = {'entity_id': ha_entity['id']} - - data_obj['color_name'] = message.data['color'] - self.ha.execute_service("light", "turn_on", data_obj) - - data_obj['dev_name'] = ha_entity['dev_name'] - self.speak_dialog('homeassistant.color.change', data=data_obj) - - @intent_handler('automation.intent') - def handle_automation_intent(self, message): - self.log.debug("Automation trigger intent on entity: " + - message.data.get("entity")) - message.data["Entity"] = message.data.get("entity") - self._handle_automation(message) - - @intent_handler('tracker.intent') - def handle_tracker_intent(self, message): - self.log.debug("Turn on intent on entity: "+message.data.get("entity")) - message.data["Entity"] = message.data.get("entity") - self._handle_tracker(message) - - @intent_handler('set.climate.intent') - def handle_set_thermostat_intent(self, message): - self.log.debug("Set thermostat intent on entity: " + - message.data.get("entity")) - message.data["Entity"] = message.data.get("entity") - message.data["Temp"] = message.data.get("temp") - self._handle_set_thermostat(message) - - @intent_handler('add.item.shopping.list.intent') - def handle_shopping_list_intent(self, message): - self.log.debug("Add : "+message.data.get("entity") + - "to the shoping list") - message.data["Entity"] = message.data.get("entity") - self._handle_shopping_list(message) - - def _handle_turn_actions(self, message): - self.log.debug("Starting Switch Intent") - entity = message.data["Entity"] - action = message.data["Action"] - self.log.debug("Entity: %s" % entity) - self.log.debug("Action: %s" % action) - - # Handle turn on/off all intent - try: - if self.voc_match(entity, "all_lights"): - domain = "light" - elif self.voc_match(entity, "all_switches"): - domain = "switch" - else: - domain = None - - if domain is not None: - ha_entity = {'dev_name': entity} - ha_data = {'entity_id': 'all'} - - self.ha.execute_service(domain, "turn_%s" % action, ha_data) - self.speak_dialog('homeassistant.device.%s' % - action, data=ha_entity) - return - # TODO: need to figure out, if this indeed throws a KeyError - except KeyError: - self.log.debug("Not turn on/off all intent") - except: - self.log.debug( - "Unexpected error in turn all intent:", exc_info()[0]) - - # Hande single entity - - ha_entity = self._find_entity( - entity, - [ - 'group', - 'light', - 'fan', - 'switch', - 'scene', - 'input_boolean', - 'climate' - ] - ) - - # Exit if entiti not found or is unavailabe - if not ha_entity or not self._check_availability(ha_entity): - return - - self.log.debug("Entity State: %s" % ha_entity['state']) - - ha_data = {'entity_id': ha_entity['id']} - - # IDEA: set context for 'turn it off' again or similar - # self.set_context('Entity', ha_entity['dev_name']) - if ha_entity['state'] == action: - self.log.debug("Entity in requested state") - self.speak_dialog('homeassistant.device.already', data={ - "dev_name": ha_entity['dev_name'], 'action': action}) - elif action == "toggle": - self.ha.execute_service("homeassistant", "toggle", - ha_data) - if(ha_entity['state'] == 'off'): - action = 'on' - else: - action = 'off' - self.speak_dialog('homeassistant.device.%s' % action, - data=ha_entity) - elif action in ["on", "off"]: - self.speak_dialog('homeassistant.device.%s' % action, - data=ha_entity) - self.ha.execute_service("homeassistant", "turn_%s" % action, - ha_data) - else: - self.speak_dialog('homeassistant.error.sorry') - return - - def _handle_light_set(self, message): - entity = message.data["entity"] - try: - brightness_req = float(message.data["Brightnessvalue"]) - if brightness_req > 100 or brightness_req < 0: - self.speak_dialog('homeassistant.brightness.badreq') - except KeyError: - brightness_req = 10.0 - brightness_value = int(brightness_req / 100 * 255) - brightness_percentage = int(brightness_req) - self.log.debug("Entity: %s" % entity) - self.log.debug("Brightness Value: %s" % brightness_value) - self.log.debug("Brightness Percent: %s" % brightness_percentage) - - ha_entity = self._find_entity(entity, ['group', 'light']) - # Exit if entiti not found or is unavailabe - if not ha_entity or not self._check_availability(ha_entity): - return - - ha_data = {'entity_id': ha_entity['id']} - - # IDEA: set context for 'turn it off again' or similar - # self.set_context('Entity', ha_entity['dev_name']) - # Set values for HA - ha_data['brightness'] = brightness_value - self.ha.execute_service("light", "turn_on", ha_data) - # Set values for mycroft reply - ha_data['dev_name'] = ha_entity['dev_name'] - ha_data['brightness'] = brightness_req - self.speak_dialog('homeassistant.brightness.dimmed', - data=ha_data) - - return - - def _handle_shopping_list(self, message): - entity = message.data["Entity"] - ha_data = {'name': entity} - self.ha.execute_service("shopping_list", "add_item", ha_data) - self.speak_dialog("homeassistant.shopping.list") - return - - def _handle_light_adjust(self, message): - entity = message.data["Entity"] - action = message.data["Action"] - brightness_req = 10.0 - brightness_value = int(brightness_req / 100 * 255) - # brightness_percentage = int(brightness_req) # debating use - self.log.debug("Entity: %s" % entity) - self.log.debug("Brightness Value: %s" % brightness_value) - - # Set the min and max brightness for bulbs. Smart bulbs - # use 0-255 integer brightness, while spoken commands will - # use 0-100% brightness. - min_brightness = 5 - max_brightness = 255 - - ha_entity = self._find_entity(entity, ['group', 'light']) - # Exit if entiti not found or is unavailabe - if not ha_entity or not self._check_availability(ha_entity): - return - ha_data = {'entity_id': ha_entity['id']} - # IDEA: set context for 'turn it off again' or similar - # self.set_context('Entity', ha_entity['dev_name']) - - if action == "down": - if ha_entity['state'] == "off": - self.speak_dialog('homeassistant.brightness.cantdim.off', - data=ha_entity) - else: - light_attrs = self.ha.find_entity_attr(ha_entity['id']) - if light_attrs['unit_measure'] is None: - self.speak_dialog( - 'homeassistant.brightness.cantdim.dimmable', - data=ha_entity) - else: - ha_data['brightness'] = light_attrs['unit_measure'] - \ - brightness_value - if ha_data['brightness'] < min_brightness: - ha_data['brightness'] = min_brightness - self.ha.execute_service("homeassistant", - "turn_on", - ha_data) - ha_data['dev_name'] = ha_entity['dev_name'] - ha_data['brightness'] = round( - 100 / max_brightness * ha_data['brightness']) - self.speak_dialog('homeassistant.brightness.decreased', - data=ha_data) - elif action == "up": - if ha_entity['state'] == "off": - self.speak_dialog( - 'homeassistant.brightness.cantdim.off', - data=ha_entity) - else: - light_attrs = self.ha.find_entity_attr(ha_entity['id']) - if light_attrs['unit_measure'] is None: - self.speak_dialog( - 'homeassistant.brightness.cantdim.dimmable', - data=ha_entity) - else: - ha_data['brightness'] = light_attrs['unit_measure'] + \ - brightness_value - if ha_data['brightness'] > max_brightness: - ha_data['brightness'] = max_brightness - self.ha.execute_service("homeassistant", - "turn_on", - ha_data) - ha_data['dev_name'] = ha_entity['dev_name'] - ha_data['brightness'] = round( - 100 / max_brightness * ha_data['brightness']) - self.speak_dialog('homeassistant.brightness.increased', - data=ha_data) - else: - self.speak_dialog('homeassistant.error.sorry') - return - - def _handle_automation(self, message): - entity = message.data["Entity"] - self.log.debug("Entity: %s" % entity) - ha_entity = self._find_entity( - entity, - ['automation', 'scene', 'script'] - ) - - # Exit if entiti not found or is unavailabe - if not ha_entity or not self._check_availability(ha_entity): - return - - ha_data = {'entity_id': ha_entity['id']} - - # IDEA: set context for 'turn it off again' or similar - # self.set_context('Entity', ha_entity['dev_name']) - - self.log.debug("Triggered automation/scene/script: {}".format(ha_data)) - if "automation" in ha_entity['id']: - self.ha.execute_service('automation', 'trigger', ha_data) - self.speak_dialog('homeassistant.automation.trigger', - data={"dev_name": ha_entity['dev_name']}) - elif "script" in ha_entity['id']: - self.speak_dialog('homeassistant.automation.trigger', - data={"dev_name": ha_entity['dev_name']}) - self.ha.execute_service("script", "turn_on", - data=ha_data) - elif "scene" in ha_entity['id']: - self.speak_dialog('homeassistant.scene.on', - data=ha_entity) - self.ha.execute_service("scene", "turn_on", - data=ha_data) - - def _handle_sensor(self, message): - entity = message.data["Entity"] - self.log.debug("Entity: %s" % entity) - - ha_entity = self._find_entity(entity, ['sensor', 'switch']) - # Exit if entiti not found or is unavailabe - if not ha_entity or not self._check_availability(ha_entity): - return - - entity = ha_entity['id'] - - # IDEA: set context for 'read it out again' or similar - # self.set_context('Entity', ha_entity['dev_name']) - - unit_measurement = self.ha.find_entity_attr(entity) - sensor_unit = unit_measurement.get('unit_measure') or '' - - sensor_name = unit_measurement['name'] - sensor_state = unit_measurement['state'] - # extract unit for correct pronounciation - # this is fully optional - try: - from quantulum3 import parser - quantulumImport = True - except ImportError: - quantulumImport = False - - if quantulumImport and unit_measurement != '': - quantity = parser.parse((u'{} is {} {}'.format( - sensor_name, sensor_state, sensor_unit))) - if len(quantity) > 0: - quantity = quantity[0] - if (quantity.unit.name != "dimensionless" and - quantity.uncertainty <= 0.5): - sensor_unit = quantity.unit.name - sensor_state = quantity.value - - try: - value = float(sensor_state) - sensor_state = nice_number(value, lang=self.language) - except ValueError: - pass - - self.speak_dialog('homeassistant.sensor', data={ - "dev_name": sensor_name, - "value": sensor_state, - "unit": sensor_unit}) - # IDEA: Add some context if the person wants to look the unit up - # Maybe also change to name - # if one wants to look up "outside temperature" - # self.set_context("SubjectOfInterest", sensor_unit) - - # In progress, still testing. - # Device location works. - # Proximity might be an issue - # - overlapping command for directions modules - # - (e.g. "How far is x from y?") - def _handle_tracker(self, message): - entity = message.data["Entity"] - self.log.debug("Entity: %s" % entity) - - ha_entity = self._find_entity(entity, ['device_tracker']) - # Exit if entiti not found or is unavailabe - if not ha_entity or not self._check_availability(ha_entity): - return - - # IDEA: set context for 'locate it again' or similar - # self.set_context('Entity', ha_entity['dev_name']) - - entity = ha_entity['id'] - dev_name = ha_entity['dev_name'] - dev_location = ha_entity['state'] - self.speak_dialog('homeassistant.tracker.found', - data={'dev_name': dev_name, - 'location': dev_location}) - - def _handle_set_thermostat(self, message): - entity = message.data["entity"] - self.log.debug("Entity: %s" % entity) - self.log.debug("This is the message data: %s" % message.data) - temperature = message.data["temp"] - self.log.debug("Temperature: %s" % temperature) - - ha_entity = self._find_entity(entity, ['climate']) - # Exit if entiti not found or is unavailabe - if not ha_entity or not self._check_availability(ha_entity): - return - - climate_data = { - 'entity_id': ha_entity['id'], - 'temperature': temperature - } - climate_attr = self.ha.find_entity_attr(ha_entity['id']) - self.ha.execute_service("climate", "set_temperature", - data=climate_data) - self.speak_dialog('homeassistant.set.thermostat', - data={ - "dev_name": climate_attr['name'], - "value": temperature, - "unit": climate_attr['unit_measure']}) - - def handle_fallback(self, message): - if not self.enable_fallback: - return False - self._setup() - if self.ha is None: - self.speak_dialog('homeassistant.error.setup') - return False - # pass message to HA-server - response = self._handle_client_exception( - self.ha.engage_conversation, - message.data.get('utterance')) - if not response: - return False - # default non-parsing answer: "Sorry, I didn't understand that" - answer = response.get('speech') - if not answer or answer == "Sorry, I didn't understand that": - return False - - asked_question = False - # TODO: maybe enable conversation here if server asks sth like - # "In which room?" => answer should be directly passed to this skill - if answer.endswith("?"): - asked_question = True - self.speak(answer, expect_response=asked_question) - return True - - def shutdown(self): - self.remove_fallback(self.handle_fallback) - super(HomeAssistantSkill, self).shutdown() - - def stop(self): - pass - - -def create_skill(): - return HomeAssistantSkill() From 715bc81e7703a4b572051b8a24c18ff493579eb7 Mon Sep 17 00:00:00 2001 From: Daniel Scicluna Date: Tue, 28 Sep 2021 11:44:25 +0200 Subject: [PATCH 13/13] rename data var --- __init__.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/__init__.py b/__init__.py index 80ceb3d9..82fdde25 100644 --- a/__init__.py +++ b/__init__.py @@ -228,7 +228,7 @@ def _handle_light_color(self, message): voc_match = entity if self.voc_match(voc_match, "all_lights"): - data_obj = {'entity_id': 'all'} + action_data = {'entity_id': 'all'} ha_entity = {'dev_name': 'all lights'} else: ha_entity = self._find_entity(entity, ['group', 'light']) @@ -236,13 +236,13 @@ def _handle_light_color(self, message): if not ha_entity or not self._check_availability(ha_entity): return - data_obj = {'entity_id': ha_entity['id']} + action_data = {'entity_id': ha_entity['id']} - data_obj['color_name'] = message.data['color'] - self.ha.execute_service("light", "turn_on", data_obj) + action_data['color_name'] = message.data['color'] + self.ha.execute_service("light", "turn_on", action_data) - data_obj['dev_name'] = ha_entity['dev_name'] - self.speak_dialog('homeassistant.color.change', data=data_obj) + action_data['dev_name'] = ha_entity['dev_name'] + self.speak_dialog('homeassistant.color.change', data=action_data) @intent_handler('automation.intent') def handle_automation_intent(self, message):