diff --git a/README.md b/README.md index c62d7290..09b577b0 100644 --- a/README.md +++ b/README.md @@ -65,6 +65,7 @@ Elatko devices are exemplarily mentioned. You can find [here](https://www.eltako * A5-38-08 (light and switch) * H5-3F-7F (cover) * [Send Message Service](https://github.com/grimmpp/home-assistant-eltako/tree/main/docs/service-send-message/readme.md) Sends any EnOcean Message. Can be used for [automatinos in Home Assistant](https://www.home-assistant.io/getting-started/automation/) so that none-EnOcean and EnOcean deviecs can be combined. + * Not supported EEPs: A5-09-0C (Air Quality), A5-38-08 (Central Command) [**Gateway**](https://github.com/grimmpp/home-assistant-eltako/tree/main/docs/gateways/readme.md) (See also [how to use gateways](https://github.com/grimmpp/home-assistant-eltako/tree/main/docs/gateway_usage/readme.md) and [multiple gateway support](https://github.com/grimmpp/home-assistant-eltako/tree/main/docs/multiple-gateway-support/readme.md)) * **Eltako FAM14** and Eltako **FGW14-USB** (based on ESP2, rs485 bus and baud rate 57600, uses library [eltako14bus](https://github.com/grimmpp/eltako14bus)) diff --git a/changes.md b/changes.md index ed795e25..ca3f2fc3 100644 --- a/changes.md +++ b/changes.md @@ -3,10 +3,11 @@ ## Version 1.4.1 Support for sending arbritrary messages * Added Service for sending arbritrary ESP2 messages * 🐞 Fix for TargetTemperatureSensor (EEP: A5-10-06 and A5-10-12) -* 🐞 Fix for unknow cover positions. +* 🐞 Fix for unknow cover positions and intermediate state + unit-tests added. * Unit-Tests added and improved for EEP A5-04-01, A5-04-02, A5-10-06, A5-10-12, A5-13-01, and F6-10-00. * EEP A5-04-03 added for Eltako FFT60 (temperature and humiditry) * EEP A5-06-01 added for light sensor (currently twilight and daylight are combinded in one illumination sensor/entity) +* Bug fixes in EEPs (in [eltako14bus library](https://github.com/grimmpp/eltako14bus)) ## Version 1.4.0 ESP3 Support (USB300) * Docs about gateway usage added. diff --git a/custom_components/eltako/config_helpers.py b/custom_components/eltako/config_helpers.py index 9a57ccc4..6abad980 100644 --- a/custom_components/eltako/config_helpers.py +++ b/custom_components/eltako/config_helpers.py @@ -123,7 +123,7 @@ def get_device_config(config: dict, id: int) -> dict: return g[CONF_DEVICES] return None -async def async_get_list_of_gateway_descriptions(hass: HomeAssistant, CONFIG_SCHEMA: dict, get_integration_config=async_integration_yaml_config, filter_out: [str]=[]) -> dict: +async def async_get_list_of_gateway_descriptions(hass: HomeAssistant, CONFIG_SCHEMA: dict, get_integration_config=async_integration_yaml_config, filter_out: list[str]=[]) -> dict: config = await async_get_home_assistant_config(hass, CONFIG_SCHEMA, get_integration_config) return get_list_of_gateway_descriptions(config, filter_out) diff --git a/custom_components/eltako/cover.py b/custom_components/eltako/cover.py index 7657e43b..106a20ae 100644 --- a/custom_components/eltako/cover.py +++ b/custom_components/eltako/cover.py @@ -241,7 +241,9 @@ def value_changed(self, msg): if self.dev_eep in [G5_3F_7F]: LOGGER.debug(f"[cover {self.dev_id}] G5_3F_7F - {decoded.__dict__}") - ## is received as response when button pushed + ## is received as response when button pushed (command was sent) + ## this message is received directly when the cover starts to move + ## when the cover results in completely open or close one of the following messages (open or closed) will appear if decoded.state == 0x02: # down self._attr_is_closing = True self._attr_is_opening = False @@ -261,7 +263,8 @@ def value_changed(self, msg): self._attr_is_closed = False self._attr_current_cover_position = 100 - ## is received when cover stops at a position + ## is received when cover stops at the desired intermediate position + ## if not close state is always open (close state should be reported with closed message above) elif decoded.time is not None and decoded.direction is not None and self._time_closes is not None and self._time_opens is not None: time_in_seconds = decoded.time / 10.0 @@ -288,10 +291,6 @@ def value_changed(self, msg): self._attr_is_closed = True self._attr_is_opening = False self._attr_is_closing = False - # elif self._attr_current_cover_position == 100: - # self._attr_is_closed = False - # self._attr_is_opening = False - # self._attr_is_closing = False else: self._attr_is_closed = False self._attr_is_opening = False diff --git a/custom_components/eltako/gateway.py b/custom_components/eltako/gateway.py index fab554a7..be1c2133 100644 --- a/custom_components/eltako/gateway.py +++ b/custom_components/eltako/gateway.py @@ -216,7 +216,7 @@ async def async_setup(self): # Command Section - async def async_service_send_message(self, event) -> None: + async def async_service_send_message(self, event, raise_exception=False) -> None: """Send an arbitrary message with the provided eep.""" LOGGER.debug(f"[Service Send Message: {event.service}] Received event data: {event.data}") @@ -252,10 +252,11 @@ async def async_service_send_message(self, event) -> None: msg = eep.encode_message(sender_id[0]) LOGGER.debug(f"[Service Send Message: {event.service}] Generated message: {msg} Serialized: {msg.serialize().hex()}") # send message - event_id = config_helpers.get_bus_event_type(self.base_id, SIGNAL_SEND_MESSAGE) - dispatcher_send(self.hass, event_id, msg) - except: + self.send_message(msg) + except Exception as e: LOGGER.error(f"[Service Send Message: {event.service}] Cannot send message.", exc_info=True, stack_info=True) + if raise_exception: + raise e diff --git a/custom_components/eltako/manifest.json b/custom_components/eltako/manifest.json index b147b1b0..aca7540f 100644 --- a/custom_components/eltako/manifest.json +++ b/custom_components/eltako/manifest.json @@ -9,6 +9,6 @@ "iot_class": "local_push", "issue_tracker": "https://github.com/grimmpp/home-assistant-eltako/issues", "loggers": ["eltako"], - "requirements": ["eltako14bus==0.0.48","enocean==0.60.1", "StrEnum", "esp2-gateway-adapter==0.1"], + "requirements": ["eltako14bus==0.0.49","enocean==0.60.1", "StrEnum", "esp2-gateway-adapter==0.1"], "version": "1.4.1" } diff --git a/docs/service-send-message/eep-params.md b/docs/service-send-message/eep-params.md new file mode 100644 index 00000000..85babf46 --- /dev/null +++ b/docs/service-send-message/eep-params.md @@ -0,0 +1,28 @@ +# Paramters for EEPs in Send Message Events +## Not Supported EEPs +* A5-09-0C +* A5-38-08 + +## Parameters for events: +* A5-04-01: humidity, learn_button, temp_availability, temperature +* A5-04-02: humidity, learn_button, temperature +* A5-04-03: humidity, learn_button, telegram_type, temperature +* A5-06-01: day_light, illumination, twilight +* A5-07-01: learn_button, pir_status, pir_status_on, support_volrage_availability, support_voltage +* A5-08-01: illumination, learn_button, occupancy_button, pir_status, supply_voltage, temperature +* A5-10-06: current_temp, mode, stand_by, target_temp +* A5-10-12: current_temperature, humidity, target_temperature +* A5-12-01: data_type, divisor, learn_button, measurement_channel, meter_reading +* A5-12-02: data_type, divisor, learn_button, measurement_channel, meter_reading +* A5-12-03: data_type, divisor, learn_button, measurement_channel, meter_reading +* A5-13-01: dawn_sensor, day_night, hemisphere, identifier, learn_button, rain_indication, sun_east, sun_south, sun_west, temperature, wind_speed +* D5-00-01: contact, learn_button +* F6-02-01: energy_bow, rocker_first_action, rocker_second_action, second_action +* F6-02-02: energy_bow, rocker_first_action, rocker_second_action, second_action +* F6-10-00: handle_position, movement +* G5-3F-7F: direction, state, time +* H5-3F-7F: command, learn_button, time +* M5-38-08: state + +## References: +Implementation of EEPs can be found [eltako14bus library](https://github.com/grimmpp/eltako14bus/blob/master/eltakobus/eep.py). diff --git a/docs/service-send-message/readme.md b/docs/service-send-message/readme.md index 109f47cb..e12a3560 100644 --- a/docs/service-send-message/readme.md +++ b/docs/service-send-message/readme.md @@ -8,7 +8,7 @@ This service is mainly inteded to combine none-EnOcean and EnOcean devices. Serv Create an [automation](https://www.home-assistant.io/getting-started/automation/) in Home Assistant. Use the upper sections to react on anything you like, dependent on you sensor/sender. -`Add Action` and search for `eltako`. It proposes a service to send messages for every available gateway. In addition you need to enter in the data section what to send. The fields `id` and `eep` must be specified. `id` stands for the sender address. (Keep in mind: bus gateways do not send in wireless network and wireless tranceivers do only have 128 hardcoded addresses to be used.) `eep` is the message format you what to use. Dependent on the eep specific information is put into the messages to be sent. You can either check in the [code](https://github.com/grimmpp/eltako14bus/blob/master/eltakobus/eep.py) or in the logs what attribute need to be set. +`Add Action` and search for `eltako`. It proposes a service to send messages for every available gateway. In addition you need to enter in the data section what to send. The fields `id` and `eep` must be specified. `id` stands for the sender address. (Keep in mind: bus gateways do not send in wireless network and wireless tranceivers do only have 128 hardcoded addresses to be used.) `eep` is the message format you what to use. Dependent on the eep specific information is put into the messages to be sent. You can either check in the [code](https://github.com/grimmpp/eltako14bus/blob/master/eltakobus/eep.py) or in this automatically generated [parameter list for EEPs](eep-params.md). In the logs you can see what parameters has been transferred and which will be set as default to 0. diff --git a/tests/mocks.py b/tests/mocks.py index ba5d02a8..834f7894 100644 --- a/tests/mocks.py +++ b/tests/mocks.py @@ -26,6 +26,7 @@ class HassMock(): def __init__(self) -> None: self.bus = BusMock() + self.loop = asyncio.get_event_loop() # def async_create_task(self, async_call): # asyncio.run( async_call ) @@ -43,6 +44,10 @@ def __init__(self): def is_active(self): return self._is_active +class EventMock(): + def __init__(self, service:str, data:dict) -> None: + self.service = service + self.data = data class GatewayMock(EnOceanGateway): def __init__(self, general_settings:dict=DEFAULT_GENERAL_SETTINGS, dev_id: int=123, base_id:AddressExpression=AddressExpression.parse('FF-AA-80-00')): diff --git a/tests/test_cover.py b/tests/test_cover_G5_3F_7F.py similarity index 80% rename from tests/test_cover.py rename to tests/test_cover_G5_3F_7F.py index 66fddd9b..74b09bd9 100644 --- a/tests/test_cover.py +++ b/tests/test_cover_G5_3F_7F.py @@ -25,8 +25,8 @@ def create_cover(self) -> EltakoCover: dev_id = AddressExpression.parse('00-00-00-01') dev_name = 'device name' device_class = "shutter" - time_closes = 3 - time_opens = 3 + time_closes = 10 + time_opens = 10 eep_string = "G5-3F-7F" sender_id = AddressExpression.parse("00-00-B1-06") @@ -79,13 +79,42 @@ def test_cover_value_changed(self): self.assertEqual(ec._attr_current_cover_position, 100) + def test_cover_intermediate_cover_positions(self): + ec = self.create_cover() + + msg = Regular4BSMessage(address=b'\x00\x00\x00\x01', status=b'\x20', data=b'\x00\x1e\x01\x0a', outgoing=False) + ec._attr_current_cover_position = 10 + ec.value_changed(msg) + self.assertEqual(ec._attr_is_closing, False) + self.assertEqual(ec._attr_is_opening, False) + self.assertEqual(ec._attr_is_closed, False) + self.assertEqual(ec._attr_current_cover_position, 40) + + msg = Regular4BSMessage(address=b'\x00\x00\x00\x01', status=b'\x20', data=b'\x00\x0a\x01\x0a', outgoing=False) + ec._attr_current_cover_position = 0 + ec.value_changed(msg) + self.assertEqual(ec._attr_is_closing, False) + self.assertEqual(ec._attr_is_opening, False) + self.assertEqual(ec._attr_is_closed, False) + self.assertEqual(ec._attr_current_cover_position, 10) + + msg = Regular4BSMessage(address=b'\x00\x00\x00\x01', status=b'\x20', data=b'\x00\x5a\x02\x0a', outgoing=False) + ec._attr_current_cover_position = 100 + ec.value_changed(msg) + self.assertEqual(ec._attr_is_closing, False) + self.assertEqual(ec._attr_is_opening, False) + self.assertEqual(ec._attr_is_closed, False) + self.assertEqual(ec._attr_current_cover_position, 10) + + + def test_open_cover(self): ec = self.create_cover() ec.open_cover() self.assertEqual( self.last_sent_command.body, - b'k\x07\x00\x04\x01\x08\x00\x00\xb1\x06\x00') + b'k\x07\x00\x0b\x01\x08\x00\x00\xb1\x06\x00') def test_close_cover(self): ec = self.create_cover() @@ -93,7 +122,7 @@ def test_close_cover(self): ec.close_cover() self.assertEqual( self.last_sent_command.body, - b'k\x07\x00\x04\x02\x08\x00\x00\xb1\x06\x00') + b'k\x07\x00\x0b\x02\x08\x00\x00\xb1\x06\x00') def test_stop_cover(self): ec = self.create_cover() @@ -110,7 +139,7 @@ def test_set_cover_position(self): ec.set_cover_position(position=50) self.assertEqual( self.last_sent_command.body, - b'k\x07\x00\x01\x02\x08\x00\x00\xb1\x06\x00') + b'k\x07\x00\x05\x02\x08\x00\x00\xb1\x06\x00') self.assertEqual(ec._attr_is_closing, True) self.assertEqual(ec._attr_is_opening, False) self.last_sent_command = None @@ -119,7 +148,7 @@ def test_set_cover_position(self): ec.set_cover_position(position=50) self.assertEqual( self.last_sent_command.body, - b'k\x07\x00\x01\x01\x08\x00\x00\xb1\x06\x00') + b'k\x07\x00\x05\x01\x08\x00\x00\xb1\x06\x00') self.assertEqual(ec._attr_is_closing, False) self.assertEqual(ec._attr_is_opening, True) self.last_sent_command = None @@ -128,7 +157,7 @@ def test_set_cover_position(self): ec.set_cover_position(position=0) self.assertEqual( self.last_sent_command.body, - b'k\x07\x00\x04\x02\x08\x00\x00\xb1\x06\x00') + b'k\x07\x00\x0b\x02\x08\x00\x00\xb1\x06\x00') self.assertEqual(ec._attr_is_closing, True) self.assertEqual(ec._attr_is_opening, False) self.last_sent_command = None @@ -137,7 +166,7 @@ def test_set_cover_position(self): ec.set_cover_position(position=100) self.assertEqual( self.last_sent_command.body, - b'k\x07\x00\x04\x01\x08\x00\x00\xb1\x06\x00') + b'k\x07\x00\x0b\x01\x08\x00\x00\xb1\x06\x00') self.assertEqual(ec._attr_is_closing, False) self.assertEqual(ec._attr_is_opening, True) self.last_sent_command = None @@ -199,4 +228,7 @@ def test_initial_loading_closed(self): self.assertEqual(ec.is_opening, False) self.assertEqual(ec.is_closing, False) self.assertEqual(ec.state, 'closed') - self.assertEqual(ec.current_cover_position, 0) \ No newline at end of file + self.assertEqual(ec.current_cover_position, 0) + + + diff --git a/tests/test_send_message_service.py b/tests/test_send_message_service.py new file mode 100644 index 00000000..122b8916 --- /dev/null +++ b/tests/test_send_message_service.py @@ -0,0 +1,77 @@ +import unittest +from mocks import * +from unittest import mock +from homeassistant.helpers import dispatcher +import inspect + +dispatcher.dispatcher_send = mock.Mock(return_value=None) + +class TestSendMessageService(unittest.IsolatedAsyncioTestCase): + + def create_gateway(self): + gateway = GatewayMock(dev_id=123) + + return gateway + + def get_all_eep_names(self): + subclasses = set() + work = [EEP] + while work: + parent = work.pop() + for child in parent.__subclasses__(): + if child not in subclasses: + subclasses.add(child) + work.append(child) + return sorted(set([s.__name__.upper().replace('_','-') for s in subclasses if len(s.__name__) == 8 and s.__name__.count('_') == 2])) + + NOT_SUPPORTED_EEPS = ['A5-09-0C', 'A5-38-08'] + + async def test_send_message(self): + g = self.create_gateway() + # Mock send_message + g.send_message = lambda *args: None + + for eep_name in self.get_all_eep_names(): + + if eep_name in self.NOT_SUPPORTED_EEPS: + continue + + event = EventMock('service_name', { + 'id': 'FF-DD-CC-BB', + 'eep': eep_name, + 'command': 1, + 'identifier': 1 + }) + + await g.async_service_send_message(event, True) + + + async def test_write_eep_params_to_docs_file(self): + text = '# Paramters for EEPs in Send Message Events' + text += '\n' + + text += "## Not Supported EEPs \n" + for eep_name in self.NOT_SUPPORTED_EEPS: + text += f"* {eep_name}\n" + text += '\n' + + text += '## Parameters for events: \n' + + for eep_name in self.get_all_eep_names(): + + if eep_name in self.NOT_SUPPORTED_EEPS: + continue + + sig = inspect.signature(EEP.find(eep_name).__init__) + eep_init_args = sorted([param.name for param in sig.parameters.values() if param.kind == param.POSITIONAL_OR_KEYWORD and param.name != 'self']) + text += f"* {eep_name}: {', '.join(eep_init_args)}\n" + + text += '\n' + text += '## References:\n' + text += 'Implementation of EEPs can be found [eltako14bus library](https://github.com/grimmpp/eltako14bus/blob/master/eltakobus/eep.py).\n' + + file='./docs/service-send-message/eep-params.md' + with open(file, 'w') as filetowrite: + filetowrite.write(text) + +