Skip to content

Commit

Permalink
Merge pull request #87 from grimmpp/feature-branch
Browse files Browse the repository at this point in the history
preparation for next release
  • Loading branch information
grimmpp authored Mar 27, 2024
2 parents 0e3a077 + cf7a7bc commit 9dd5c27
Show file tree
Hide file tree
Showing 21 changed files with 443 additions and 92 deletions.
12 changes: 9 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,14 @@ Elatko devices are exemplarily mentioned. You can find [here](https://www.eltako
* Binary sensor
* F6-02-01 ([Rocker switch](https://github.com/grimmpp/home-assistant-eltako/tree/main/docs/rocker_switch/readme.md), FTS14EM)
* F6-02-02 ([Rocker switch](https://github.com/grimmpp/home-assistant-eltako/tree/main/docs/rocker_switch/readme.md))
* F6-10-00 (Window handle, FTS14EM)
* F6-10-00 (Window handle, classic switches or contacs via FTS14EM, window and door contacts like FTKE, supported states: open, closed)
* D5-00-01 ([Contact sensor](https://github.com/grimmpp/home-assistant-eltako/tree/main//docs/window_sensor_setup_FTS14EM.md), FTS14EM) incl. signal inverter
* A5-07-01 (Occupancy sensor)
* Sensor
* A5-04-01 (Temperature and Humidity Sensor)
* A5-04-02 (Temperature and Humidity Sensor e.g.: FLGTF, FLT58)
* A5-04-02 (Temperature and Humidity Sensor e.g.: FLGTF, FLT58, FFT60)
* A5-04-03 (Temperature and Humidity Sensor e.g.: FFT60)
* A5-06-01 (Light - Twilight and Illumination)
* A5-07-01 (Occupancy sensor)
* A5-08-01 (Light-, Temperature-, Occupancy Sensor e.g.: FABH65S, FBH65, FBH65S, FBH65TF)
* A5-09-0C (Air Quality / VOC⁠ (Volatile Organic Compounds) e.g. [FLGTF](https://github.com/grimmpp/home-assistant-eltako/tree/main/docs/flgtf_temp_humidity_air_quality/readme.md))
Expand All @@ -37,7 +39,7 @@ Elatko devices are exemplarily mentioned. You can find [here](https://www.eltako
* A5-12-02 (Automated meter reading - gas, F3Z14D)
* A5-12-03 (Automated meter reading - water, F3Z14D)
* A5-13-01 (Weather station, FWG14)
* F6-10-00 (Window handle, FTS14EM)
* F6-10-00 (Window handle, classic switches or contacs via FTS14EM, window and door contacts like FTKE, supported states: open, closed, tilt)
* [Light](https://github.com/grimmpp/home-assistant-eltako/tree/main/docs/lights-tutorial/readme.md)
* A5-38-08 (Dimmable Light: Central command - gateway, FUD14)
* M5-38-08 (Switchable Light: Eltako relay, FSR14)
Expand All @@ -59,6 +61,10 @@ Elatko devices are exemplarily mentioned. You can find [here](https://www.eltako
* [Climate](https://github.com/grimmpp/home-assistant-eltako/tree/main/docs/heating-and-cooling/readme.md) (**Experimental** Feedback is welcome.)
* A5-10-06 (Eltako FAE14, FHK14, F4HK14, F2L14, FHK61, FME14)
* [Teach-In Buttons](https://github.com/grimmpp/home-assistant-eltako/tree/main/docs/teach_in_buttons/readme.md)
* A5-10-06, A5-10-12 (climate/thermostats)
* 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.

[**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
8 changes: 8 additions & 0 deletions changes.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# Changes and Feature List

## 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.
* 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)

## Version 1.4.0 ESP3 Support (USB300)
* Docs about gateway usage added.
* Added EEPs F6-02-01 and F6-02-02 as sender EEP for lights so that regular switch commands can be sent from Home Assistant.
Expand Down
1 change: 1 addition & 0 deletions custom_components/eltako/button.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@

EEP_WITH_TEACH_IN_BUTTONS = {
A5_10_06: b'\x40\x30\x0D\x85', # climate
A5_10_12: b'\x40\x90\x0D\x80', # climate
A5_38_08: b'\xE0\x40\x0D\x80', # light
H5_3F_7F: b'\xFF\xF8\x0D\x80', # cover
# F6_02_01 # What button to take?
Expand Down
67 changes: 16 additions & 51 deletions custom_components/eltako/cover.py
Original file line number Diff line number Diff line change
Expand Up @@ -230,53 +230,6 @@ def stop_cover(self, **kwargs: Any) -> None:
self.schedule_update_ha_state()


def value_changed(self, msg):
"""Update the internal state of the cover."""
try:
decoded = self._dev_eep.decode_message(msg)
except Exception as e:
LOGGER.warning("Could not decode message: %s", str(e))
return

if self._dev_eep in [G5_3F_7F]:
if decoded.state == 0x02: # down
self._attr_is_closing = True
self._attr_is_opening = False
elif decoded.state == 0x50: # closed
self._attr_is_opening = False
self._attr_is_closing = False
self._attr_is_closed = True
self._attr_current_cover_position = 0
elif decoded.state == 0x01: # up
self._attr_is_opening = True
self._attr_is_closing = False
self._attr_is_closed = False
elif decoded.state == 0x70: # open
self._attr_is_opening = False
self._attr_is_closing = False
self._attr_is_closed = False
self._attr_current_cover_position = 100
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

if decoded.direction == 0x01: # up
self._attr_current_cover_position = min(self._attr_current_cover_position + int(time_in_seconds / self._time_opens * 100.0), 100)


else: # down
self._attr_current_cover_position = max(self._attr_current_cover_position - int(time_in_seconds / self._time_closes * 100.0), 0)

if self._attr_current_cover_position == 0:
self._attr_is_closed = True

self._attr_is_closing = False
self._attr_is_opening = False

LOGGER.debug(f"[cover {self.dev_id}] state: {self.state}, opening: {self.is_opening}, closing: {self.is_closing}, closed: {self.is_closed}, position: {self.current_cover_position}")

self.schedule_update_ha_state()


def value_changed(self, msg):
"""Update the internal state of the cover."""
try:
Expand Down Expand Up @@ -309,19 +262,31 @@ def value_changed(self, msg):
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

if decoded.direction == 0x01: # up

if decoded.direction == 0x01: # up
# If the latest state is unknown, the cover position
# will be set to None, therefore we have to guess
# the initial position.
if self._attr_current_cover_position is None:
self._attr_current_cover_position = 0

self._attr_current_cover_position = min(self._attr_current_cover_position + int(time_in_seconds / self._time_opens * 100.0), 100)
self._attr_is_opening = True
self._attr_is_closing = False
self._attr_is_closed = None

else: # down
# If the latest state is unknown, the cover position
# will be set to None, therefore we have to guess
# the initial position.
if self._attr_current_cover_position is None:
self._attr_current_cover_position = 100

else: # down
self._attr_current_cover_position = max(self._attr_current_cover_position - int(time_in_seconds / self._time_closes * 100.0), 0)
self._attr_is_opening = False
self._attr_is_closing = True
self._attr_is_closed = None

if self._attr_current_cover_position == 0:
self._attr_is_closed = True
self._attr_is_opening = False
Expand Down
59 changes: 59 additions & 0 deletions custom_components/eltako/gateway.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from eltakobus.message import ESP2Message, RPSMessage, Regular1BSMessage, Regular4BSMessage, EltakoPoll, prettify

from eltakobus.util import AddressExpression
from eltakobus.eep import EEP

from enocean.communicators import SerialCommunicator
from enocean.protocol.packet import RadioPacket, RORG, Packet
Expand Down Expand Up @@ -205,6 +206,64 @@ async def async_setup(self):
self.hass, event_id, self._callback_send_message_to_serial_bus
)

# Register home assistant service for sending arbitrary telegrams.
#
# The service will be registered for each gateway, as the user
# might have different gateways that cause the eltako relays
# only to react on them.
service_name = f"gateway_{self._attr_dev_id}_send_message"
self.hass.services.async_register(DOMAIN, service_name, self.async_service_send_message)


# Command Section
async def async_service_send_message(self, event) -> None:
"""Send an arbitrary message with the provided eep."""
LOGGER.debug(f"[Service Send Message: {event.service}] Received event data: {event.data}")

try:
sender_id_str = event.data.get("id", None)
sender_id:AddressExpression = AddressExpression.parse(sender_id_str)
except:
LOGGER.error(f"[Service Send Message: {event.service}] No valid sender id defined. (Given sender id: {sender_id_str})")
return

try:
sender_eep_str = event.data.get("eep", None)
sender_eep:EEP = EEP.find(sender_eep_str)
except:
LOGGER.error(f"[Service Send Message: {event.service}] No valid sender id defined. (Given sender id: {sender_id_str})")
return

# prepare all arguements for eep constructor
import inspect
sig = inspect.signature(sender_eep.__init__)
eep_init_args = [param.name for param in sig.parameters.values() if param.kind == param.POSITIONAL_OR_KEYWORD]
knargs = {filter_key:event.data[filter_key] for filter_key in eep_init_args if filter_key in event.data and filter_key != 'self'}
LOGGER.debug(f"[Service Send Message: {event.service}] Provided EEP ({sender_eep.__name__}) args: {knargs})")
uknargs = {filter_key:0 for filter_key in eep_init_args if filter_key not in event.data and filter_key != 'self'}
LOGGER.debug(f"[Service Send Message: {event.service}] Missing EEP ({sender_eep.__name__}) args: {uknargs})")
eep_args = knargs
eep_args.update(uknargs)

eep:EEP = sender_eep(**eep_args)

try:
# create message
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:
LOGGER.error(f"[Service Send Message: {event.service}] Cannot send message.", exc_info=True, stack_info=True)



def send_message(self, msg: ESP2Message):
"""Put message on RS485 bus. First the message is put onto HA event bus so that other automations can react on messages."""
event_id = config_helpers.get_bus_event_type(self.base_id, SIGNAL_SEND_MESSAGE)
dispatcher_send(self.hass, event_id, msg)


def unload(self):
"""Disconnect callbacks established at init time."""
Expand Down
4 changes: 2 additions & 2 deletions 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.47","enocean==0.60.1", "StrEnum", "esp2-gateway-adapter==0.1"],
"version": "1.3.9"
"requirements": ["eltako14bus==0.0.48","enocean==0.60.1", "StrEnum", "esp2-gateway-adapter==0.1"],
"version": "1.4.1"
}
2 changes: 2 additions & 0 deletions custom_components/eltako/schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -149,6 +149,8 @@ class SensorSchema(EltakoPlatformSchema):

CONF_EEP_SUPPORTED = [A5_04_01.eep_string,
A5_04_02.eep_string,
A5_04_03.eep_string,
A5_06_01.eep_string,
A5_07_01.eep_string,
A5_08_01.eep_string,
A5_09_0C.eep_string,
Expand Down
9 changes: 7 additions & 2 deletions custom_components/eltako/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -344,7 +344,7 @@ async def async_setup_entry(
entities.append(EltakoMeterSensor(platform, gateway, dev_conf.id, dev_name, dev_conf.eep, SENSOR_DESC_WATER_CUMULATIVE, tariff=(tariff - 1)))
entities.append(EltakoMeterSensor(platform, gateway, dev_conf.id, dev_name, dev_conf.eep, SENSOR_DESC_WATER_CURRENT, tariff=(tariff - 1)))

elif dev_conf.eep in [A5_04_02, A5_10_12, A5_04_01]:
elif dev_conf.eep in [A5_04_01, A5_04_02, A5_04_03, A5_10_12]:

entities.append(EltakoTemperatureSensor(platform, gateway, dev_conf.id, dev_name, dev_conf.eep))
entities.append(EltakoHumiditySensor(platform, gateway, dev_conf.id, dev_name, dev_conf.eep))
Expand All @@ -371,6 +371,11 @@ async def async_setup_entry(
entities.append(EltakoBatteryVoltageSensor(platform, gateway, dev_conf.id, dev_name, dev_conf.eep))
# _pir_status => as binary sensor

elif dev_conf.eep in [A5_06_01]:
entities.append(EltakoIlluminationSensor(platform, gateway, dev_conf.id, dev_name, dev_conf.eep))
#TODO: add twilight
#TODO: add daylight
# both are currently combined in illumination

except Exception as e:
LOGGER.warning("[%s] Could not load configuration", platform)
Expand Down Expand Up @@ -757,7 +762,7 @@ def value_changed(self, msg: ESP2Message):
LOGGER.warning("[Target Temperature Sensor %s] Could not decode message: %s", self.dev_id, str(e))
return

self._attr_target_temperature = round( 2*decoded.target_temperature, 0)/2
self._attr_native_value = round(2 * decoded.target_temperature, 0) / 2

self.schedule_update_ha_state()

Expand Down
Empty file.
33 changes: 33 additions & 0 deletions docs/service-send-message/readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Send Message Service

This service is mainly inteded to combine none-EnOcean and EnOcean devices. Services which cannot communicate nativcely because they are based on different communication protocols, you can use automations in Home Assistant to e.g. receive trigger from an none-EnOcean sender and send a EnOcean message to an relay or sync states of a thermostat. ...

## Configuration

<img src="send_message_automation_screenshot.png" height="300"/>

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.

<img src="send_message_logs_screenshot.png" />

Values of e.g. other sensers can also be dynamically added by
```
alias: send message
description: ""
trigger: []
condition: []
action:
- service: eltako.gateway_3_send_message
metadata: {}
data:
id: FF-DD-00-01
eep: A5-10-06
current_temp: {{state_attr('climate.my_other_brands_smart_thermostat_1293127', 'current_temperature') }}
target_temp: {{state_attr('climate.my_other_brands_smart_thermostat_1293127', 'target_temperature') }}
enabled: true
mode: single
```

By triggering this example, from above, manually you will send a message via gateway 3 containing the current and target temperature of the thermostat entity 1293127.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 2 additions & 0 deletions eltakodevice_discovery/readme.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
# Eltako Device and Sensor Discovery

# DEPRECATED: Check out [EnOnocean Device Manager](https://github.com/grimmpp/enocean-device-manager)

Main purpose of this tool is to programmatically prepare the Home Assistance configuration as good as possible.

## Limitation
Expand Down
46 changes: 46 additions & 0 deletions tests/test_sensor_A5_04_01.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import unittest
from custom_components.eltako.sensor import *
from mocks import HassMock
from unittest import mock
from mocks import *
from homeassistant.helpers.entity import Entity
from homeassistant.const import Platform
from custom_components.eltako.binary_sensor import EltakoBinarySensor
from eltakobus import *

# mock update of Home Assistant
Entity.schedule_update_ha_state = mock.Mock(return_value=None)
# EltakoBinarySensor.hass.bus.fire is mocked by class HassMock


class TestSensor_A5_04_01(unittest.TestCase):

msg1 = Regular4BSMessage (address=b'\xFF\xFF\x00\x80', data=b'\xaa\x00\x00\x0d', status=0x00)

def create_temperature_sensor(self) -> EltakoTemperatureSensor:
gateway = GatewayMock()
dev_id = AddressExpression.parse("FF-FF-00-80")
dev_name = "device name"
dev_eep = EEP.find("A5-04-01")
s = EltakoTemperatureSensor(Platform.SENSOR, gateway, dev_id, dev_name, dev_eep)
return s

def create_humidity_sensor(self) -> EltakoHumiditySensor:
gateway = GatewayMock()
dev_id = AddressExpression.parse("FF-FF-00-80")
dev_name = "device name"
dev_eep = EEP.find("A5-04-01")
s = EltakoHumiditySensor(Platform.SENSOR, gateway, dev_id, dev_name, dev_eep)
return s

def test_temperature_sensor_A5_04_02(self):
s_temp = self.create_temperature_sensor()

s_temp.value_changed(self.msg1)
self.assertEqual(s_temp.native_value, 0.0)

def test_humidity_sensor_A5_04_02(self):
s_hum = self.create_humidity_sensor()

s_hum.value_changed(self.msg1)
self.assertEqual(s_hum.native_value, 0.0)
Loading

0 comments on commit 9dd5c27

Please sign in to comment.