Skip to content

Commit

Permalink
Merge pull request #89 from grimmpp/feature-branch
Browse files Browse the repository at this point in the history
added test + bug fixes
  • Loading branch information
grimmpp authored Mar 28, 2024
2 parents cd2a8d8 + b0e1a67 commit e52b294
Show file tree
Hide file tree
Showing 11 changed files with 167 additions and 23 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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))
Expand Down
3 changes: 2 additions & 1 deletion changes.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion custom_components/eltako/config_helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
11 changes: 5 additions & 6 deletions custom_components/eltako/cover.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down
9 changes: 5 additions & 4 deletions custom_components/eltako/gateway.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}")

Expand Down Expand Up @@ -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



Expand Down
2 changes: 1 addition & 1 deletion custom_components/eltako/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
28 changes: 28 additions & 0 deletions docs/service-send-message/eep-params.md
Original file line number Diff line number Diff line change
@@ -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).
2 changes: 1 addition & 1 deletion docs/service-send-message/readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

<img src="send_message_logs_screenshot.png" />

Expand Down
5 changes: 5 additions & 0 deletions tests/mocks.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 )
Expand All @@ -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')):
Expand Down
50 changes: 41 additions & 9 deletions tests/test_cover.py → tests/test_cover_G5_3F_7F.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -79,21 +79,50 @@ 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()

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()
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down Expand Up @@ -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)
self.assertEqual(ec.current_cover_position, 0)



77 changes: 77 additions & 0 deletions tests/test_send_message_service.py
Original file line number Diff line number Diff line change
@@ -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)


0 comments on commit e52b294

Please sign in to comment.