diff --git a/docs/source/agent-framework/driver-framework/home-assistant/HomeAssistantDriver.rst b/docs/source/agent-framework/driver-framework/home-assistant/HomeAssistantDriver.rst index 2e441b6e24..ac6d51fa10 100644 --- a/docs/source/agent-framework/driver-framework/home-assistant/HomeAssistantDriver.rst +++ b/docs/source/agent-framework/driver-framework/home-assistant/HomeAssistantDriver.rst @@ -61,11 +61,11 @@ Registry Configuration +++++++++++++++++++++++ Registry file can contain one single device and its attributes or a logical group of devices and its -attributes. Each entry should include the full entity id of the device, including but not limited to home assistant provided prefix +attributes. Each entry should include the full entity id of the device, including but not limited to home assistant provided prefix such as "light.", "climate." etc. The driver uses these prefixes to convert states into integers. Like mentioned before, the driver can only control lights and thermostats but can get data from all devices controlled by home assistant - + Each entry in a registry file should also have a 'Entity Point' and a unique value for 'Volttron Point Name'. The 'Entity ID' maps to the device instance, the 'Entity Point' extracts the attribute or state, and 'Volttron Point Name' determines the name of that point as it appears in VOLTTRON. Attributes can be located in the developer tools in the Home Assistant GUI. @@ -108,7 +108,7 @@ id 'light.example': .. note:: When using a single registry file to represent a logical group of multiple physical entities, make sure the -"Volttron Point Name" is unique within a single registry file. +"Volttron Point Name" is unique within a single registry file. For example, if a registry file contains entities with id 'light.instance1' and 'light.instance2' the entry for the attribute brightness for these two light instances could @@ -175,3 +175,12 @@ Upon completion, initiate the platform driver. Utilize the listener agent to ver [{'light_brightness': 254, 'state': 'on'}, {'light_brightness': {'type': 'integer', 'tz': 'UTC', 'units': 'int'}, 'state': {'type': 'integer', 'tz': 'UTC', 'units': 'On / Off'}}] + +Running Tests ++++++++++++++++++++++++ +To run tests on the VOLTTRON home assistant driver you need to create a helper in your home assistant instance. This can be done by going to **Settings > Devices & services > Helpers > Create Helper > Toggle**. Name this new toggle **volttrontest**. After that run the pytest from the root of your VOLTTRON file. + +.. code-block:: bash + pytest volttron/services/core/PlatformDriverAgent/tests/test_home_assistant.py + +If everything works, you will see 6 passed tests. diff --git a/services/core/PlatformDriverAgent/platform_driver/interfaces/home_assistant.py b/services/core/PlatformDriverAgent/platform_driver/interfaces/home_assistant.py index 018c145887..6088100f24 100644 --- a/services/core/PlatformDriverAgent/platform_driver/interfaces/home_assistant.py +++ b/services/core/PlatformDriverAgent/platform_driver/interfaces/home_assistant.py @@ -1,25 +1,39 @@ # -*- coding: utf-8 -*- {{{ -# ===----------------------------------------------------------------------=== +# vim: set fenc=utf-8 ft=python sw=4 ts=4 sts=4 et: # -# Component of Eclipse VOLTTRON +# Copyright 2020, Battelle Memorial Institute. # -# ===----------------------------------------------------------------------=== +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at # -# Copyright 2023 Battelle Memorial Institute -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may not -# use this file except in compliance with the License. You may obtain a copy -# of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 +# http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. # -# ===----------------------------------------------------------------------=== +# This material was prepared as an account of work sponsored by an agency of +# the United States Government. Neither the United States Government nor the +# United States Department of Energy, nor Battelle, nor any of their +# employees, nor any jurisdiction or organization that has cooperated in the +# development of these materials, makes any warranty, express or +# implied, or assumes any legal liability or responsibility for the accuracy, +# completeness, or usefulness or any information, apparatus, product, +# software, or process disclosed, or represents that its use would not infringe +# privately owned rights. Reference herein to any specific commercial product, +# process, or service by trade name, trademark, manufacturer, or otherwise +# does not necessarily constitute or imply its endorsement, recommendation, or +# favoring by the United States Government or any agency thereof, or +# Battelle Memorial Institute. The views and opinions of authors expressed +# herein do not necessarily state or reflect those of the +# United States Government or any agency thereof. +# +# PACIFIC NORTHWEST NATIONAL LABORATORY operated by +# BATTELLE for the UNITED STATES DEPARTMENT OF ENERGY +# under Contract DE-AC05-76RL01830 # }}} @@ -28,7 +42,7 @@ import json import sys from platform_driver.interfaces import BaseInterface, BaseRegister, BasicRevert -from volttron.platform.agent import utils # added this to pull from config store +from volttron.platform.agent import utils from volttron.platform.vip.agent import Agent import logging import requests @@ -80,7 +94,7 @@ def __init__(self, **kwargs): self.port = None self.units = None - def configure(self, config_dict, registry_config_str): # grabbing from config + def configure(self, config_dict, registry_config_str): self.ip_address = config_dict.get("ip_address", None) self.access_token = config_dict.get("access_token", None) self.port = config_dict.get("port", None) @@ -141,6 +155,20 @@ def _set_point(self, point_name, value): _log.error(error_msg) raise ValueError(error_msg) + elif "input_boolean." in register.entity_id: + if entity_point == "state": + if isinstance(register.value, int) and register.value in [0, 1]: + if register.value == 1: + self.set_input_boolean(register.entity_id, "on") + elif register.value == 0: + self.set_input_boolean(register.entity_id, "off") + else: + error_msg = f"State value for {register.entity_id} should be an integer value of 1 or 0" + _log.info(error_msg) + raise ValueError(error_msg) + else: + _log.info(f"Currently, input_booleans only support state") + # Changing thermostat values. elif "climate." in register.entity_id: if entity_point == "state": @@ -200,7 +228,6 @@ def _scrape_all(self): if "climate." in entity_id: # handling thermostats. if entity_point == "state": state = entity_data.get("state", None) - # Giving thermostat states an equivalent number. if state == "off": register.value = 0 @@ -224,7 +251,7 @@ def _scrape_all(self): register.value = attribute result[register.point_name] = attribute # handling light states - elif "light." in entity_id: + elif "light." or "input_boolean." in entity_id: # Checks for lights or input bools since they have the same states. if entity_point == "state": state = entity_data.get("state", None) # Converting light states to numbers. @@ -269,10 +296,7 @@ def parse_config(self, config_dict): self.point_name = regDef['Volttron Point Name'] self.units = regDef['Units'] description = regDef.get('Notes', '') - - default_value = str(regDef.get("Starting Value", 'sin')).strip() - if not default_value: - default_value = None + default_value = ("Starting Value") type_name = regDef.get("Type", 'string') reg_type = type_mapping.get(type_name, str) attributes = regDef.get('Attributes', {}) @@ -375,3 +399,23 @@ def change_brightness(self, entity_id, value): } _post_method(url, headers, payload, f"set brightness of {entity_id} to {value}") + + def set_input_boolean(self, entity_id, state): + service = 'turn_on' if state == 'on' else 'turn_off' + url = f"http://{self.ip_address}:{self.port}/api/services/input_boolean/{service}" + headers = { + "Authorization": f"Bearer {self.access_token}", + "Content-Type": "application/json", + } + + payload = { + "entity_id": entity_id + } + + response = requests.post(url, headers=headers, json=payload) + + # Optionally check for a successful response + if response.status_code == 200: + print(f"Successfully set {entity_id} to {state}") + else: + print(f"Failed to set {entity_id} to {state}: {response.text}") diff --git a/services/core/PlatformDriverAgent/tests/test_home_assistant.py b/services/core/PlatformDriverAgent/tests/test_home_assistant.py index 6fdb26443b..0d6c59a13e 100644 --- a/services/core/PlatformDriverAgent/tests/test_home_assistant.py +++ b/services/core/PlatformDriverAgent/tests/test_home_assistant.py @@ -1,26 +1,41 @@ # -*- coding: utf-8 -*- {{{ -# ===----------------------------------------------------------------------=== +# vim: set fenc=utf-8 ft=python sw=4 ts=4 sts=4 et: # -# Component of Eclipse VOLTTRON +# Copyright 2020, Battelle Memorial Institute. # -# ===----------------------------------------------------------------------=== +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at # -# Copyright 2023 Battelle Memorial Institute -# -# Licensed under the Apache License, Version 2.0 (the "License"); you may not -# use this file except in compliance with the License. You may obtain a copy -# of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 +# http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT -# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the -# License for the specific language governing permissions and limitations -# under the License. +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# This material was prepared as an account of work sponsored by an agency of +# the United States Government. Neither the United States Government nor the +# United States Department of Energy, nor Battelle, nor any of their +# employees, nor any jurisdiction or organization that has cooperated in the +# development of these materials, makes any warranty, express or +# implied, or assumes any legal liability or responsibility for the accuracy, +# completeness, or usefulness or any information, apparatus, product, +# software, or process disclosed, or represents that its use would not infringe +# privately owned rights. Reference herein to any specific commercial product, +# process, or service by trade name, trademark, manufacturer, or otherwise +# does not necessarily constitute or imply its endorsement, recommendation, or +# favoring by the United States Government or any agency thereof, or +# Battelle Memorial Institute. The views and opinions of authors expressed +# herein do not necessarily state or reflect those of the +# United States Government or any agency thereof. # -# ===----------------------------------------------------------------------=== +# PACIFIC NORTHWEST NATIONAL LABORATORY operated by +# BATTELLE for the UNITED STATES DEPARTMENT OF ENERGY +# under Contract DE-AC05-76RL01830 # }}} + import json import logging import pytest @@ -38,7 +53,8 @@ utils.setup_logging() logger = logging.getLogger(__name__) -HOMEASSISTANT_DEVICE_TOPIC = "devices/home_assistant" +# To run these tests, create a helper toggle named volttrontest in your Home Assistant instance. +# This can be done by going to Settings > Devices & services > Helpers > Create Helper > Toggle HOMEASSISTANT_TEST_IP = "" ACCESS_TOKEN = "" PORT = "" @@ -57,14 +73,14 @@ def test_get_point(volttron_instance, config_store): expected_values = 0 agent = volttron_instance.dynamic_agent - result = agent.vip.rpc.call(PLATFORM_DRIVER, 'get_point', 'home_assistant', 'light_state').get(timeout=20) + result = agent.vip.rpc.call(PLATFORM_DRIVER, 'get_point', 'home_assistant', 'bool_state').get(timeout=20) assert result == expected_values, "The result does not match the expected result." # The default value for this fake light is 3. If the test cannot reach out to home assistant, -# the value will default to 3 meking the test fail. +# the value will default to 3 making the test fail. def test_data_poll(volttron_instance: PlatformWrapper, config_store): - expected_values = [{'light_state': 0}, {'light_state': 1}] + expected_values = [{'bool_state': 0}, {'bool_state': 1}] agent = volttron_instance.dynamic_agent result = agent.vip.rpc.call(PLATFORM_DRIVER, 'scrape_all', 'home_assistant').get(timeout=20) assert result in expected_values, "The result does not match the expected result." @@ -73,9 +89,9 @@ def test_data_poll(volttron_instance: PlatformWrapper, config_store): # Turn on the light. Light is automatically turned off every 30 seconds to allow test to turn # it on and receive the correct value. def test_set_point(volttron_instance, config_store): - expected_values = {'light_state': 1} + expected_values = {'bool_state': 1} agent = volttron_instance.dynamic_agent - agent.vip.rpc.call(PLATFORM_DRIVER, 'set_point', 'home_assistant', 'light_state', 1) + agent.vip.rpc.call(PLATFORM_DRIVER, 'set_point', 'home_assistant', 'bool_state', 1) gevent.sleep(10) result = agent.vip.rpc.call(PLATFORM_DRIVER, 'scrape_all', 'home_assistant').get(timeout=20) assert result == expected_values, "The result does not match the expected result." @@ -89,9 +105,9 @@ def config_store(volttron_instance, platform_driver): registry_config = "homeassistant_test.json" registry_obj = [{ - "Entity ID": "light.fake_light", + "Entity ID": "input_boolean.volttrontest", "Entity Point": "state", - "Volttron Point Name": "light_state", + "Volttron Point Name": "bool_state", "Units": "On / Off", "Units Details": "off: 0, on: 1", "Writable": True,