Initial commit
This commit is contained in:
0
fhem/README.md
Normal file
0
fhem/README.md
Normal file
101
fhem/__init__.py
Normal file
101
fhem/__init__.py
Normal file
@@ -0,0 +1,101 @@
|
||||
"""FHEM integration"""
|
||||
|
||||
import logging
|
||||
import voluptuous as vol
|
||||
import asyncio
|
||||
|
||||
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'
|
||||
DOMAIN = 'fhem'
|
||||
DATA_FHEM = "data_fhem"
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
vol.Required(CONF_HOST): cv.string,
|
||||
vol.Optional(CONF_PORT, default=7072): cv.port,
|
||||
vol.Required(CONF_CUL_DEVICE_NAME): cv.string,
|
||||
})
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
|
||||
async def async_setup(hass, config):
|
||||
connection = FhemConnection(hass, config)
|
||||
hass.data[DATA_FHEM] = connection
|
||||
await connection.start()
|
||||
return True
|
||||
|
||||
|
||||
class FhemConnection:
|
||||
|
||||
def __init__(self, hass, config):
|
||||
self.hass = hass
|
||||
self._host = config[DOMAIN][CONF_HOST]
|
||||
self._port = config[DOMAIN][CONF_PORT]
|
||||
self._cul_device_name = config[DOMAIN][CONF_CUL_DEVICE_NAME]
|
||||
|
||||
self.connected = False
|
||||
self.reconnect_time_start = 1
|
||||
self.reconnect_time_max = 60
|
||||
self.reconnect_time = self.reconnect_time_start
|
||||
self.devices = {}
|
||||
self._run = False
|
||||
self._writer = None
|
||||
|
||||
async def start(self):
|
||||
self._run = True
|
||||
self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, self.stop)
|
||||
self.hass.loop.create_task(self._connection())
|
||||
|
||||
async def stop(self):
|
||||
self._run = False
|
||||
self.connected = False
|
||||
|
||||
async def _connection(self):
|
||||
try:
|
||||
reader, writer = await asyncio.open_connection(self._host, self._port)
|
||||
_LOGGER.info("Connected to FHEM {}:{}".format(self._host, self._port))
|
||||
self._writer = writer
|
||||
self.connected = True
|
||||
self.reconnect_time = self.reconnect_time_start
|
||||
writer.writelines([
|
||||
"displayattr .*\n".encode(),
|
||||
"inform on\n".encode(),
|
||||
])
|
||||
while self._run:
|
||||
line = await reader.readline()
|
||||
line = line.decode()
|
||||
_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))
|
||||
self.connected = False
|
||||
await asyncio.sleep(self.reconnect_time)
|
||||
self.reconnect_time = min(2 * self.reconnect_time, self.reconnect_time)
|
||||
self.hass.loop.create_task(self._connection())
|
||||
|
||||
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())
|
||||
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())
|
||||
|
||||
def write_line(self, line):
|
||||
if self._writer:
|
||||
line += '\n'
|
||||
self._writer.write(line.encode())
|
||||
|
||||
def fhem_set(self, id, *arguments):
|
||||
"""
|
||||
Send command to FHEM using this device
|
||||
:param arguments: string or list of strings containing command parameters
|
||||
"""
|
||||
arguments = " ".join([str(a) for a in arguments])
|
||||
self._writer.write("set {} {}\n".format(id, arguments).encode())
|
||||
0
fhem/cover.py
Normal file
0
fhem/cover.py
Normal file
147
fhem/light.py
Normal file
147
fhem/light.py
Normal file
@@ -0,0 +1,147 @@
|
||||
"""Support for lights from FHEM"""
|
||||
|
||||
import voluptuous as vol
|
||||
import logging
|
||||
|
||||
from homeassistant.components.light import ATTR_BRIGHTNESS, PLATFORM_SCHEMA, SUPPORT_BRIGHTNESS, Light
|
||||
from homeassistant.const import CONF_NAME
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
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,
|
||||
vol.Required(CONF_NAME): cv.string,
|
||||
})
|
||||
|
||||
|
||||
async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
|
||||
"""Set up lights for KNX platform."""
|
||||
connection = hass.data[DATA_FHEM]
|
||||
#_LOGGER.error("FHEM platform config\n" + str(config))
|
||||
|
||||
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
|
||||
async_add_entities([light])
|
||||
|
||||
|
||||
class FhemLight(Light):
|
||||
|
||||
def __init__(self, connection, name, ids, dimmer=False):
|
||||
self._brightness = None
|
||||
self.connection = connection
|
||||
self._dimmer = dimmer
|
||||
self._ids = ids
|
||||
self._name = name
|
||||
self._available = True
|
||||
|
||||
@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
|
||||
|
||||
@property
|
||||
def supported_features(self):
|
||||
"""Flag supported features."""
|
||||
flags = 0
|
||||
if self._dimmer:
|
||||
flags |= SUPPORT_BRIGHTNESS
|
||||
return flags
|
||||
|
||||
async def async_turn_on(self, **kwargs):
|
||||
brightness = kwargs.get(ATTR_BRIGHTNESS, self.brightness)
|
||||
if brightness is None:
|
||||
brightness = 255
|
||||
|
||||
if self._dimmer:
|
||||
self.connection.fhem_set(self._ids[0], brightness / 255 * 100)
|
||||
else:
|
||||
self.connection.fhem_set(self._ids[0], 'on')
|
||||
|
||||
async def async_turn_off(self, **kwargs):
|
||||
self.connection.fhem_set(self._ids[0], 'off')
|
||||
|
||||
async def line_received(self, line):
|
||||
_LOGGER.info("FHEM line received (device): " + self.name + ": " + line)
|
||||
if line.startswith('dim:'):
|
||||
self._available = True
|
||||
_, new_dim_state, new_level = line.split(':')
|
||||
new_level = new_level.strip().lower()
|
||||
new_dim_state = new_dim_state.strip().lower()
|
||||
assert new_dim_state == 'stop' or new_dim_state == 'up' or new_dim_state == 'down'
|
||||
if new_dim_state == 'stop':
|
||||
if new_level == 'on':
|
||||
self._brightness = 255
|
||||
elif new_level == 'off':
|
||||
self._brightness = 0
|
||||
else:
|
||||
new_level = int(float(new_level)) # first convert from string to floating point then truncate
|
||||
assert 0 <= new_level <= 100
|
||||
self._brightness = int(new_level / 100 * 255)
|
||||
await self.async_update_ha_state()
|
||||
elif line.startswith('level:') and not self._dimmer:
|
||||
self._available = True
|
||||
_, new_level = line.split(':')
|
||||
new_level = new_level.strip().lower()
|
||||
if new_level == 'on':
|
||||
self._brightness = 255
|
||||
elif new_level == 'off':
|
||||
self._brightness = 0
|
||||
else:
|
||||
try:
|
||||
new_level = int(float(new_level)) # first convert from string to floating point then truncate
|
||||
assert 0 <= new_level <= 100
|
||||
self._brightness = int(new_level / 100 * 255)
|
||||
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: <br>"
|
||||
"<b>{0}</b>".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: <br>"
|
||||
"<b>{0}</b>".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()
|
||||
9
fhem/manifest.json
Normal file
9
fhem/manifest.json
Normal file
@@ -0,0 +1,9 @@
|
||||
|
||||
{
|
||||
"domain": "fhem",
|
||||
"name": "FHEM",
|
||||
"documentation": "",
|
||||
"dependencies": [],
|
||||
"codeowners": ["@mabau"],
|
||||
"requirements": []
|
||||
}
|
||||
0
fhem/switch.py
Normal file
0
fhem/switch.py
Normal file
Reference in New Issue
Block a user