diff --git a/config_creation/fhem.yaml b/config_creation/fhem.yaml index 9f3286a..08065db 100644 --- a/config_creation/fhem.yaml +++ b/config_creation/fhem.yaml @@ -1,5 +1,8 @@ +# ---------------------------------- Lights ------------------------------------ + light: + - name: Schlafzimmer Deckenlampe dimmer: True fhem_ids: @@ -9,6 +12,8 @@ light: - Schlafzimmer_Deckenlampe_Sw1_V_02 groups: - bedroom + + - name: Arbeitszimmer Martin Deckenlampe dimmer: True fhem_ids: @@ -18,6 +23,8 @@ light: - ArbeitszimmerMartin_Deckenlampe_Sw1_V_02 groups: - office_martin + + - name: Arbeitszimmer Rebecca Deckenlampe dimmer: True fhem_ids: @@ -28,31 +35,64 @@ light: groups: - office_rebecca -#switch: -#- name: Bad Lüfter -# dimmer: False -# fhem_ids: -# - Bad_Luefter -# -#cover: -#- name: Arbeitszimmer Martin Rollo -# fhem_ids: -# - ArbeitszimmerMartin_Rollo -# groups: -# - office_martin -#- name: Schlafzimmer Rollo klein -# fhem_ids: -# - Schlafzimmer_RolloKlein -# groups: -# - bedroom -#- name: Schlafzimmer Rollo groß -# fhem_ids: -# - Schlafzimmer_RolloGross -# groups: -# - bedroom -#- name: Arbeitszimmer Rebecca Rollo -# fhem_ids: -# - ArbeitszimmerRebecca_Rollo -# groups: -# - office_rebecca -# \ No newline at end of file + +# ---------------------------------- Covers ------------------------------------ + +cover: + + +- name: Arbeitszimmer Martin Rollo + fhem_ids: + - ArbeitszimmerMartin_Rollo + groups: + - office_martin + + +- name: Schlafzimmer Rollo klein + fhem_ids: + - Schlafzimmer_RolloKlein + groups: + - bedroom + + +- name: Schlafzimmer Rollo groß + fhem_ids: + - Schlafzimmer_RolloGross + groups: + - bedroom + + +- name: Arbeitszimmer Rebecca Rollo + fhem_ids: + - ArbeitszimmerRebecca_Rollo + groups: + - office_rebecca + + +# ---------------------------------- Switches ------------------------------------ + +switch: +- name: Bad Lüfter + fhem_ids: + - Bad_Luefter + + + +# ------------------------------ Motion Sensors ------------------------------------ + +binary_sensor: + +- name: Arbeitszimmer Martin Bewegungsmelder Batterie + fhem_ids: + - ArbeitszimmerMartin_Bewegungsmelder + fhem_sensor_type: battery +- name: Arbeitszimmer Martin Bewegungsmelder Bewegung + fhem_ids: + - ArbeitszimmerMartin_Bewegungsmelder + fhem_sensor_type: motion + +sensor: +- name: Arbeitszimmer Martin Bewegungsmelder Helligkeit + fhem_ids: + - ArbeitszimmerMartin_Bewegungsmelder + fhem_sensor_type: brightness diff --git a/config_creation/ui-lovelace.yaml b/config_creation/ui-lovelace.yaml index f5216d0..66d278c 100644 --- a/config_creation/ui-lovelace.yaml +++ b/config_creation/ui-lovelace.yaml @@ -1,6 +1,10 @@ resources: - type: js url: /local/custom_ui/state-card-custom-cover.js + - type: js + url: /local/custom_ui/lovelace-toggle-lock-entity-row/toggle-lock-entity-row.js + - type: module + url: /local/custom_ui/mini-graph-card-bundle.js?v=0.4.3 title: Home views: - cards: @@ -90,14 +94,66 @@ views: title: Verbrauch type: entities - entities: - - switch.trockner - - switch.waschmaschine - - switch.spulmaschine - - switch.backofen - - switch.herd_phase_1 - - switch.herd_phase_2 - - switch.herd_phase_3 + - entity: switch.trockner + type: 'custom:toggle-lock-entity-row' + - entity: switch.waschmaschine + type: 'custom:toggle-lock-entity-row' + - entity: switch.spulmaschine + type: 'custom:toggle-lock-entity-row' + - entity: switch.backofen + type: 'custom:toggle-lock-entity-row' + - entity: switch.herd_phase_1 + type: 'custom:toggle-lock-entity-row' + - entity: switch.herd_phase_2 + type: 'custom:toggle-lock-entity-row' + - entity: switch.herd_phase_3 + type: 'custom:toggle-lock-entity-row' show_header_toggle: false title: Sicherheitsabschaltung type: entities + - animate: true + entities: + - sensor.waschmaschine_verbrauch + - sensor.trockner_verbrauch + name: Waschen & Trocknen + type: 'custom:mini-graph-card' + - animate: true + entities: + - entity: sensor.spulmaschine_verbrauch + name: Spühlmaschine + - entity: sensor.backofen_verbrauch + name: Backofen + - entity: sensor.herd_phase_1_verbrauch + name: Herd P1 + - entity: sensor.herd_phase_2_verbrauch + name: Herd P2 + - entity: sensor.herd_phase_3_verbrauch + name: Herd P3 + hours_to_show: 8 + name: Küche + points_per_hour: 4 + type: 'custom:mini-graph-card' + - animate: true + entities: + - entity: sensor.fritz_box_7490_kbyte_sec_received + name: Down + - entity: sensor.fritz_box_7490_kbyte_sec_sent + name: Up + hours_to_show: 2 + name: Internet + points_per_hour: 30 + type: 'custom:mini-graph-card' title: Admin + - cards: + - entities: + - light.arbeitszimmer_martin_deckenlampe + - name: Rollo Arbeitszimmer + entity: cover.arbeitszimmer_martin_rollo + type: 'custom:state-card-custom-cover' + - binary_sensor.arbeitszimmer_martin_bewegungsmelder_bewegung + - sensor.arbeitszimmer_martin_bewegungsmelder_helligkeit + - switch.bad_lufter + show_header_toggle: false + title: Test + type: entities + title: Test diff --git a/fhem/__init__.py b/fhem/__init__.py index 81dd237..63cb262 100644 --- a/fhem/__init__.py +++ b/fhem/__init__.py @@ -3,12 +3,14 @@ import logging import voluptuous as vol import asyncio - +from collections import defaultdict import homeassistant.helpers.config_validation as cv from homeassistant.const import CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_STOP _LOGGER = logging.getLogger(__name__) CONF_CUL_DEVICE_NAME = 'cul_device_name' +CONF_FHEM_SENSOR_TYPE = 'fhem_sensor_type' +CONF_FHEM_IDS = 'fhem_ids' DOMAIN = 'fhem' DATA_FHEM = "data_fhem" CONFIG_SCHEMA = vol.Schema({ @@ -39,9 +41,18 @@ class FhemConnection: self.reconnect_time_start = 1 self.reconnect_time_max = 60 self.reconnect_time = self.reconnect_time_start - self.devices = {} + self._devices = defaultdict(list) self._run = False self._writer = None + self._connection_last_state = 'UNKNOWN' + + def register_device(self, id, d): + self._devices[id].append(d) + + async def _update_all_devices(self): + for device_list in self._devices.values(): + for device in device_list: + await device.async_update_ha_state() async def start(self): self._run = True @@ -56,10 +67,11 @@ class FhemConnection: try: reader, writer = await asyncio.open_connection(self._host, self._port) _LOGGER.info("Connected to FHEM {}:{}".format(self._host, self._port)) + self._connection_last_state = 'CONNECTED' + self._writer = writer self.connected = True - for device in self.devices.values(): - await device.async_update_ha_state() + await self._update_all_devices() self.reconnect_time = self.reconnect_time_start writer.writelines([ @@ -72,10 +84,14 @@ class FhemConnection: _LOGGER.debug("FHEM received line: {}".format(line)) await self._process_line(line) except OSError: - _LOGGER.warning("Connection to FHEM failed {}:{}".format(self._host, self._port)) + if self._connection_last_state != 'FAILED': + self.hass.components.persistent_notification.async_create("FHEM connection failed", + title="No FHEM connection") + _LOGGER.error("Connection to FHEM failed {}:{}".format(self._host, self._port)) + self._connection_last_state = 'FAILED' + self.connected = False - for device in self.devices.values(): - await device.async_update_ha_state() + await self._update_all_devices() await asyncio.sleep(self.reconnect_time) self.reconnect_time = min(2 * self.reconnect_time, self.reconnect_time) self.hass.loop.create_task(self._connection()) @@ -83,14 +99,15 @@ class FhemConnection: async def _process_line(self, line): if line.startswith(self._cul_device_name + " "): # Status update message _, device_name, command = line.split(" ", 2) - if device_name in self.devices: - await self.devices[device_name].line_received(command.strip()) + for device in self._devices[device_name]: + _LOGGER.debug("FHEM line received (device): " + device_name + ": " + line) + await device.line_received(command.strip()) else: # potential response to displayattr split_line = line.split(" ", 1) if len(split_line) == 2: device_name, command = split_line - if device_name in self.devices: - await self.devices[device_name].line_received(command.strip()) + for device in self._devices[device_name]: + await device.line_received(command.strip()) def write_line(self, line): if self._writer: @@ -104,3 +121,20 @@ class FhemConnection: """ arguments = " ".join([str(a) for a in arguments]) self._writer.write("set {} {}\n".format(id, arguments).encode()) + + +def device_error_reporting(hass, received_line, component_type, component_name): + if received_line.startswith('overheat'): + overheat = received_line.split(':')[1] + overheat = overheat.strip().lower() + assert overheat == 'on' or overheat == 'off' + if overheat == 'on': + text = "FHEM: {} overheated:
{}".format(component_type, component_name) + hass.components.persistent_notification.async_create(text, title="{} overheat".format(component_type)) + elif received_line.startswith('overload'): + overload = received_line.split(':')[1] + overload = overload.strip().lower() + assert overload == 'on' or overload == 'off' + if overload == 'on': + text = "FHEM: {} overloaded:
{}".format(component_type, component_name) + hass.components.persistent_notification.async_create(text, title="{} overloaded".format(component_type)) diff --git a/fhem/binary_sensor.py b/fhem/binary_sensor.py new file mode 100644 index 0000000..e22d560 --- /dev/null +++ b/fhem/binary_sensor.py @@ -0,0 +1,116 @@ +"""Support for covers from FHEM""" + +import voluptuous as vol +import logging + +from homeassistant.components.binary_sensor import PLATFORM_SCHEMA, BinarySensorDevice, \ + DEVICE_CLASS_MOTION, DEVICE_CLASS_OPENING, DEVICE_CLASS_BATTERY +from homeassistant.const import CONF_NAME +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.event import async_call_later +from homeassistant.core import callback +from . import DATA_FHEM, device_error_reporting, CONF_FHEM_SENSOR_TYPE, CONF_FHEM_IDS + + +_LOGGER = logging.getLogger(__name__) + + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_FHEM_IDS): vol.All(cv.ensure_list, [cv.string]), + vol.Required(CONF_NAME): cv.string, + vol.Required(CONF_FHEM_SENSOR_TYPE): vol.In(('motion', 'cover', 'battery')), +}) + + +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + connection = hass.data[DATA_FHEM] + + sensor = FhemBinarySensor(connection, + config[CONF_NAME], + config[CONF_FHEM_IDS], + config[CONF_FHEM_SENSOR_TYPE]) + + for dev_id in config[CONF_FHEM_IDS]: + connection.register_device(dev_id, sensor) + async_add_entities([sensor]) + + +class FhemBinarySensor(BinarySensorDevice): + + def __init__(self, connection, name, ids, sensor_type): + self._on = None + self.connection = connection + self._ids = ids + self._name = name + self._type = sensor_type + self._available = True + self._state = None + self._unsubscribe_motion_off = None + + @property + def name(self): + return self._name + + @property + def should_poll(self): + """No polling needed.""" + return False + + @property + def available(self) -> bool: + return self._available and self.connection.connected + + @property + def is_on(self): + return self._state + + async def line_received(self, line): + if self._type == 'motion' and line.startswith('motion'): + self._available = True + self._state = True + self._start_motion_off_callback(delay_in_seconds=31) + await self.async_update_ha_state() + elif self._type == 'cover' and line.startswith('cover'): + self._available = True + _, new_value = line.split(':') + self._state = not (new_value.strip().lower() == 'closed') + await self.async_update_ha_state() + elif self._type == 'battery' and line.startswith('battery'): + self._available = True + _, new_value = line.split(':') + self._state = not (new_value.strip().lower() == 'ok') + await self.async_update_ha_state() + elif line.startswith('ResndFail') or line.startswith('MISSING ACK'): + self._available = False + await self.async_update_ha_state() + else: + device_error_reporting(self.hass, line, component_type="Switch", component_name=self.entity_id) + + @property + def device_state_attributes(self): + return None + + def _start_motion_off_callback(self, delay_in_seconds): + self._stop_motion_off_callback() + self._unsubscribe_motion_off = async_call_later(self.hass, delay_in_seconds, self._stop_motion_callback) + + def _stop_motion_off_callback(self): + if self._unsubscribe_motion_off is not None: + self._unsubscribe_motion_off() + self._unsubscribe_motion_off = None + + @callback + def _stop_motion_callback(self, now): + self._state = False + self.schedule_update_ha_state() + + @property + def device_class(self): + if self._type == 'motion': + return DEVICE_CLASS_MOTION + elif self._type == 'cover': + return DEVICE_CLASS_OPENING + elif self._type == 'battery': + return DEVICE_CLASS_BATTERY + else: + return None diff --git a/fhem/cover.py b/fhem/cover.py index e69de29..af2ac6b 100644 --- a/fhem/cover.py +++ b/fhem/cover.py @@ -0,0 +1,107 @@ +"""Support for covers from FHEM""" + +import voluptuous as vol +import logging + +from homeassistant.components.cover import PLATFORM_SCHEMA, CoverDevice, SUPPORT_OPEN, SUPPORT_CLOSE, \ + SUPPORT_SET_POSITION, SUPPORT_STOP, ATTR_POSITION +from homeassistant.const import CONF_NAME +import homeassistant.helpers.config_validation as cv +from . import DATA_FHEM, device_error_reporting, CONF_FHEM_IDS + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_FHEM_IDS): vol.All(cv.ensure_list, [cv.string]), + vol.Required(CONF_NAME): cv.string, +}) + + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + connection = hass.data[DATA_FHEM] + + cover = FhemCover(connection, config[CONF_NAME], config[CONF_FHEM_IDS]) + for dev_id in config[CONF_FHEM_IDS]: + connection.register_device(dev_id, cover) + async_add_entities([cover]) + + +class FhemCover(CoverDevice): + + def __init__(self, connection, name, ids): + self._position = None + self.connection = connection + self._ids = ids + self._name = name + self._available = True + + @property + def name(self): + return self._name + + @property + def should_poll(self): + """No polling needed.""" + return False + + @property + def available(self) -> bool: + return self._available and self.connection.connected + + @property + def supported_features(self): + """Flag supported features.""" + return SUPPORT_OPEN | SUPPORT_CLOSE | SUPPORT_SET_POSITION | SUPPORT_STOP + + @property + def current_cover_position(self): + return self._position + + @property + def is_closed(self): + """Return if the cover is closed.""" + if self._position is None: + return None + return self._position <= 25 + + async def async_close_cover(self, **kwargs): + await self.async_set_cover_position(**{ATTR_POSITION: 0}) + + async def async_open_cover(self, **kwargs): + await self.async_set_cover_position(**{ATTR_POSITION: 100}) + + async def async_set_cover_position(self, **kwargs): + """Move the cover to a specific position.""" + if ATTR_POSITION in kwargs: + position = kwargs[ATTR_POSITION] + self._position = position + self.connection.fhem_set(self._ids[0], int(position)) + + async def async_stop_cover(self, **kwargs): + """Stop the cover.""" + self.connection.fhem_set(self._ids[0], 'stop') + + async def line_received(self, line): + if line.startswith('motor:'): + self._available = True + _, new_motor_state, new_position = line.split(':') + new_position = new_position.strip().lower() + new_motor_state = new_motor_state.strip().lower() + assert new_motor_state == 'stop' or new_motor_state == 'up' or new_motor_state == 'down' + if new_motor_state == 'stop': + if new_position == 'on': + self._position = 100 + elif new_position == 'off': + self._position = 0 + else: + new_position = int(float(new_position)) # first convert from string to floating point then truncate + assert 0 <= new_position <= 100 + self._position = new_position + await self.async_update_ha_state() + + elif line.startswith('ResndFail') or line.startswith('MISSING ACK'): + self._available = False + await self.async_update_ha_state() + else: + device_error_reporting(self.hass, line, component_type="Cover", component_name=self.entity_id) diff --git a/fhem/light.py b/fhem/light.py index b067fa5..5cd3c27 100644 --- a/fhem/light.py +++ b/fhem/light.py @@ -3,18 +3,16 @@ import voluptuous as vol import logging -from homeassistant.components.light import ATTR_BRIGHTNESS, PLATFORM_SCHEMA, SUPPORT_BRIGHTNESS, Light +from homeassistant.components.light import ATTR_BRIGHTNESS, PLATFORM_SCHEMA, SUPPORT_BRIGHTNESS, Light, ATTR_TRANSITION from homeassistant.const import CONF_NAME import homeassistant.helpers.config_validation as cv +from . import DATA_FHEM, device_error_reporting, CONF_FHEM_IDS + +CONF_DIMMER = 'dimmer' -from . import DATA_FHEM _LOGGER = logging.getLogger(__name__) - -CONF_FHEM_IDS = 'fhem_ids' -CONF_DIMMER = 'dimmer' - PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ vol.Required(CONF_FHEM_IDS): vol.All(cv.ensure_list, [cv.string]), vol.Optional(CONF_DIMMER, default=False): cv.boolean, @@ -25,10 +23,9 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): """Set up lights for KNX platform.""" connection = hass.data[DATA_FHEM] - light = FhemLight(connection, config[CONF_NAME], config[CONF_FHEM_IDS], dimmer=config[CONF_DIMMER]) for dev_id in config[CONF_FHEM_IDS]: - connection.devices[dev_id] = light + connection.register_device(dev_id, light) async_add_entities([light]) @@ -42,28 +39,15 @@ class FhemLight(Light): self._name = name self._available = True + @property + def name(self): + return self._name + @property def should_poll(self): """No polling needed.""" return False - @property - def brightness(self): - return self._brightness - - @property - def is_on(self): - """Return true if light is on.""" - if self._brightness is not None: - return self._brightness > 0 - else: - return None - - @property - def name(self): - """Return the name of the KNX device.""" - return self._name - @property def available(self) -> bool: return self._available and self.connection.connected @@ -76,21 +60,40 @@ class FhemLight(Light): flags |= SUPPORT_BRIGHTNESS return flags + @property + def brightness(self): + return self._brightness + + @property + def is_on(self): + """Return true if light is on.""" + if self._brightness is not None: + return self._brightness > 0 + else: + return None + async def async_turn_on(self, **kwargs): brightness = kwargs.get(ATTR_BRIGHTNESS, self.brightness) + transition_time = kwargs.get(ATTR_TRANSITION, None) + if brightness is None: brightness = 255 if self._dimmer: - self.connection.fhem_set(self._ids[0], brightness / 255 * 100) + if transition_time is not None: + # zero in the middle is the time until light is switched off, + # which is disabled here when passing 0 + self.connection.fhem_set(self._ids[0], brightness / 255 * 100, 0, transition_time) + else: + self.connection.fhem_set(self._ids[0], brightness / 255 * 100) else: self.connection.fhem_set(self._ids[0], 'on') + self._brightness = brightness async def async_turn_off(self, **kwargs): self.connection.fhem_set(self._ids[0], 'off') async def line_received(self, line): - _LOGGER.debug("FHEM line received (device): " + self.name + ": " + line) if line.startswith('dim:'): self._available = True _, new_dim_state, new_level = line.split(':') @@ -123,24 +126,8 @@ class FhemLight(Light): except ValueError: pass await self.async_update_ha_state() - elif line.startswith('overheat'): - overheat = line.split(':')[1] - overheat = overheat.strip().lower() - assert overheat == 'on' or overheat == 'off' - if overheat == 'on': - self.hass.components.persistent_notification.async_create( - "FHEM: Light overheated:
" - "{0}".format(self.entity_id), - title="Light overheat") - elif line.startswith('overload'): - overload = line.split(':')[1] - overload = overload.strip().lower() - assert overload == 'on' or overload == 'off' - if overload == 'on': - self.hass.components.persistent_notification.async_create( - "FHEM: Light overloaded:
" - "{0}".format(self.entity_id), - title="Light overloaded") elif line.startswith('ResndFail') or line.startswith('MISSING ACK'): self._available = False await self.async_update_ha_state() + else: + device_error_reporting(self.hass, line, component_type="Light", component_name=self.entity_id) diff --git a/fhem/sensor.py b/fhem/sensor.py new file mode 100644 index 0000000..f4d433e --- /dev/null +++ b/fhem/sensor.py @@ -0,0 +1,83 @@ +"""Support for covers from FHEM""" + +import voluptuous as vol +import logging + +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import CONF_NAME +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity +from . import DATA_FHEM, device_error_reporting, CONF_FHEM_SENSOR_TYPE, CONF_FHEM_IDS + + +_LOGGER = logging.getLogger(__name__) + + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_FHEM_IDS): vol.All(cv.ensure_list, [cv.string]), + vol.Required(CONF_NAME): cv.string, + vol.Optional(CONF_FHEM_SENSOR_TYPE, default='brightness'): vol.In(('brightness',)), +}) + + +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + connection = hass.data[DATA_FHEM] + + sensor = FhemSensor(connection, + config[CONF_NAME], + config[CONF_FHEM_IDS], + config[CONF_FHEM_SENSOR_TYPE]) + for dev_id in config[CONF_FHEM_IDS]: + connection.register_device(dev_id, sensor) + async_add_entities([sensor]) + + +class FhemSensor(Entity): + + def __init__(self, connection, name, ids, sensor_type): + self._on = None + self.connection = connection + self._ids = ids + self._name = name + self._type = sensor_type + self._available = True + self._state = None + self._unsubscribe_motion_off = None + + @property + def name(self): + return self._name + + @property + def should_poll(self): + """No polling needed.""" + return False + + @property + def available(self) -> bool: + return self._available and self.connection.connected + + @property + def state(self): + return self._state + + async def line_received(self, line): + if self._type == 'brightness' and line.startswith('brightness'): + self._available = True + _, new_value = line.split(':') + self._state = int(float(new_value) / 255 * 100) + await self.async_update_ha_state() + elif line.startswith('ResndFail') or line.startswith('MISSING ACK'): + self._available = False + await self.async_update_ha_state() + else: + device_error_reporting(self.hass, line, component_type="Sensor", component_name=self.entity_id) + + @property + def device_state_attributes(self): + return None + + @property + def unit_of_measurement(self): + if self._type == 'brightness': + return '%' diff --git a/fhem/switch.py b/fhem/switch.py index e69de29..db6f0fb 100644 --- a/fhem/switch.py +++ b/fhem/switch.py @@ -0,0 +1,75 @@ +"""Support for covers from FHEM""" + +import voluptuous as vol +import logging + +from homeassistant.components.switch import SwitchDevice, PLATFORM_SCHEMA +from homeassistant.const import CONF_NAME +import homeassistant.helpers.config_validation as cv +from . import DATA_FHEM, device_error_reporting, CONF_FHEM_IDS + +_LOGGER = logging.getLogger(__name__) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Required(CONF_FHEM_IDS): vol.All(cv.ensure_list, [cv.string]), + vol.Required(CONF_NAME): cv.string, +}) + + +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + connection = hass.data[DATA_FHEM] + switch = FhemSwitch(connection, config[CONF_NAME], config[CONF_FHEM_IDS]) + for dev_id in config[CONF_FHEM_IDS]: + connection.register_device(dev_id, switch) + async_add_entities([switch]) + + +class FhemSwitch(SwitchDevice): + + def __init__(self, connection, name, ids): + self._on = None + self.connection = connection + self._ids = ids + self._name = name + self._available = True + + @property + def name(self): + return self._name + + @property + def should_poll(self): + """No polling needed.""" + return False + + @property + def available(self) -> bool: + return self._available and self.connection.connected + + @property + def is_on(self): + """Return true if device is on.""" + return self._on + + async def async_turn_on(self, **kwargs): + """Turn the device on.""" + self.connection.fhem_set(self._ids[0], 'on') + + async def async_turn_off(self, **kwargs): + """Turn the device off.""" + self.connection.fhem_set(self._ids[0], 'off') + + async def line_received(self, line): + if line.startswith('level:'): + _, new_state = line.split(':') + new_state = new_state.strip().lower() + if new_state in ('on', '100'): + self._on = True + if new_state in ('off', '0'): + self._on = False + await self.async_update_ha_state() + elif line.startswith('ResndFail') or line.startswith('MISSING ACK'): + self._available = False + await self.async_update_ha_state() + else: + device_error_reporting(self.hass, line, component_type="Switch", component_name=self.entity_id) diff --git a/todo b/todo index 166fe7f..6da4cc8 100644 --- a/todo +++ b/todo @@ -9,18 +9,21 @@ - KNX config files -> add to git [ok] - - configure frontend + - configure frontend [ok] - add scenes - - add brighter/darker action + - add brighter/darker action (service!) - fix door light [ok] - check out motion detectors in frontend - check out shutters in frontend Frontend: - - change cover state card for half open + - change cover state card for half open [ok] FHEM: - - add FHEM shutters + - reconnection & message reporting code! + - add FHEM shutters [ok] + - motion detector [ok] + - service for device stuff (up/down time, auto-off stuff) - check FHEM reconnection - what happens when FHEM stops? - what happens when stick is pulled? @@ -30,6 +33,7 @@ Owntracks Other: + - grouped motion sensors (last motion where and when) - media player card: https://github.com/kalkih/mini-media-player