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