diff --git a/.coveragerc b/.coveragerc index b615c4250f8..8ef830c8a8a 100644 --- a/.coveragerc +++ b/.coveragerc @@ -142,6 +142,7 @@ omit = homeassistant/components/dovado/* homeassistant/components/downloader/* homeassistant/components/dweet/* + homeassistant/components/ebusd/* homeassistant/components/ecoal_boiler/* homeassistant/components/ecobee/* homeassistant/components/ecovacs/* @@ -686,4 +687,4 @@ exclude_lines = # Don't complain if tests don't hit defensive assertion code: raise AssertionError - raise NotImplementedError + raise NotImplementedError \ No newline at end of file diff --git a/homeassistant/components/ebusd/.translations/ebusd.en.json b/homeassistant/components/ebusd/.translations/ebusd.en.json new file mode 100644 index 00000000000..16ab79fc582 --- /dev/null +++ b/homeassistant/components/ebusd/.translations/ebusd.en.json @@ -0,0 +1,7 @@ +{ + "state": { + "day": "Day", + "night": "Night", + "auto": "Automatic" + } +} \ No newline at end of file diff --git a/homeassistant/components/ebusd/.translations/ebusd.it.json b/homeassistant/components/ebusd/.translations/ebusd.it.json new file mode 100644 index 00000000000..d0b95daaafa --- /dev/null +++ b/homeassistant/components/ebusd/.translations/ebusd.it.json @@ -0,0 +1,7 @@ +{ + "state": { + "day": "Giorno", + "night": "Notte", + "auto": "Automatico" + } +} \ No newline at end of file diff --git a/homeassistant/components/ebusd/__init__.py b/homeassistant/components/ebusd/__init__.py new file mode 100644 index 00000000000..951e6f13b91 --- /dev/null +++ b/homeassistant/components/ebusd/__init__.py @@ -0,0 +1,118 @@ +""" +Support for Ebusd daemon for communication with eBUS heating systems. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/ebus/ +""" + +from datetime import timedelta +import logging +import socket + +import voluptuous as vol + +from homeassistant.const import ( + CONF_NAME, CONF_HOST, CONF_PORT, CONF_MONITORED_CONDITIONS) +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.discovery import load_platform +from homeassistant.util import Throttle + +from .const import (DOMAIN, SENSOR_TYPES) + +REQUIREMENTS = ['ebusdpy==0.0.16'] + +_LOGGER = logging.getLogger(__name__) + +DEFAULT_NAME = 'ebusd' +DEFAULT_PORT = 8888 +CONF_CIRCUIT = 'circuit' +CACHE_TTL = 900 +SERVICE_EBUSD_WRITE = 'ebusd_write' + +MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=15) + +CONFIG_SCHEMA = vol.Schema({ + DOMAIN: vol.Schema({ + vol.Required(CONF_CIRCUIT): cv.string, + vol.Required(CONF_HOST): cv.string, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Optional(CONF_MONITORED_CONDITIONS, default=[]): vol.All( + cv.ensure_list, [vol.In(SENSOR_TYPES['700'])]) + }) +}, extra=vol.ALLOW_EXTRA) + + +def setup(hass, config): + """Set up the eBusd component.""" + conf = config[DOMAIN] + name = conf[CONF_NAME] + circuit = conf[CONF_CIRCUIT] + monitored_conditions = conf.get(CONF_MONITORED_CONDITIONS) + server_address = ( + conf.get(CONF_HOST), conf.get(CONF_PORT)) + + try: + _LOGGER.debug("Ebusd component setup started.") + import ebusdpy + ebusdpy.init(server_address) + hass.data[DOMAIN] = EbusdData(server_address, circuit) + + sensor_config = { + 'monitored_conditions': monitored_conditions, + 'client_name': name, + 'sensor_types': SENSOR_TYPES[circuit] + } + load_platform(hass, 'sensor', DOMAIN, sensor_config, config) + + hass.services.register( + DOMAIN, SERVICE_EBUSD_WRITE, hass.data[DOMAIN].write) + + _LOGGER.debug("Ebusd component setup completed.") + return True + except (socket.timeout, socket.error): + return False + + +class EbusdData: + """Get the latest data from Ebusd.""" + + def __init__(self, address, circuit): + """Initialize the data object.""" + self._circuit = circuit + self._address = address + self.value = {} + + @Throttle(MIN_TIME_BETWEEN_UPDATES) + def update(self, name, stype): + """Call the Ebusd API to update the data.""" + import ebusdpy + + try: + _LOGGER.debug("Opening socket to ebusd %s", name) + command_result = ebusdpy.read( + self._address, self._circuit, name, stype, CACHE_TTL) + if command_result is not None: + if 'ERR:' in command_result: + _LOGGER.warning(command_result) + else: + self.value[name] = command_result + except RuntimeError as err: + _LOGGER.error(err) + raise RuntimeError(err) + + def write(self, call): + """Call write methon on ebusd.""" + import ebusdpy + name = call.data.get('name') + value = call.data.get('value') + + try: + _LOGGER.debug("Opening socket to ebusd %s", name) + command_result = ebusdpy.write( + self._address, self._circuit, name, value) + if command_result is not None: + if 'done' not in command_result: + _LOGGER.warning('Write command failed: %s', name) + except RuntimeError as err: + _LOGGER.error(err) diff --git a/homeassistant/components/ebusd/const.py b/homeassistant/components/ebusd/const.py new file mode 100644 index 00000000000..c36981c5278 --- /dev/null +++ b/homeassistant/components/ebusd/const.py @@ -0,0 +1,100 @@ +"""Constants for ebus component.""" +DOMAIN = 'ebusd' + +# SensorTypes: +# 0='decimal', 1='time-schedule', 2='switch', 3='string', 4='value;status' + +SENSOR_TYPES = { + '700': { + 'ActualFlowTemperatureDesired': + ['Hc1ActualFlowTempDesired', '°C', 'mdi:thermometer', 0], + 'MaxFlowTemperatureDesired': + ['Hc1MaxFlowTempDesired', '°C', 'mdi:thermometer', 0], + 'MinFlowTemperatureDesired': + ['Hc1MinFlowTempDesired', '°C', 'mdi:thermometer', 0], + 'PumpStatus': + ['Hc1PumpStatus', None, 'mdi:toggle-switch', 2], + 'HCSummerTemperatureLimit': + ['Hc1SummerTempLimit', '°C', 'mdi:weather-sunny', 0], + 'HolidayTemperature': + ['HolidayTemp', '°C', 'mdi:thermometer', 0], + 'HWTemperatureDesired': + ['HwcTempDesired', '°C', 'mdi:thermometer', 0], + 'HWTimerMonday': + ['hwcTimer.Monday', None, 'mdi:timer', 1], + 'HWTimerTuesday': + ['hwcTimer.Tuesday', None, 'mdi:timer', 1], + 'HWTimerWednesday': + ['hwcTimer.Wednesday', None, 'mdi:timer', 1], + 'HWTimerThursday': + ['hwcTimer.Thursday', None, 'mdi:timer', 1], + 'HWTimerFriday': + ['hwcTimer.Friday', None, 'mdi:timer', 1], + 'HWTimerSaturday': + ['hwcTimer.Saturday', None, 'mdi:timer', 1], + 'HWTimerSunday': + ['hwcTimer.Sunday', None, 'mdi:timer', 1], + 'WaterPressure': + ['WaterPressure', 'bar', 'mdi:water-pump', 0], + 'Zone1RoomZoneMapping': + ['z1RoomZoneMapping', None, 'mdi:label', 0], + 'Zone1NightTemperature': + ['z1NightTemp', '°C', 'mdi:weather-night', 0], + 'Zone1DayTemperature': + ['z1DayTemp', '°C', 'mdi:weather-sunny', 0], + 'Zone1HolidayTemperature': + ['z1HolidayTemp', '°C', 'mdi:thermometer', 0], + 'Zone1RoomTemperature': + ['z1RoomTemp', '°C', 'mdi:thermometer', 0], + 'Zone1ActualRoomTemperatureDesired': + ['z1ActualRoomTempDesired', '°C', 'mdi:thermometer', 0], + 'Zone1TimerMonday': + ['z1Timer.Monday', None, 'mdi:timer', 1], + 'Zone1TimerTuesday': + ['z1Timer.Tuesday', None, 'mdi:timer', 1], + 'Zone1TimerWednesday': + ['z1Timer.Wednesday', None, 'mdi:timer', 1], + 'Zone1TimerThursday': + ['z1Timer.Thursday', None, 'mdi:timer', 1], + 'Zone1TimerFriday': + ['z1Timer.Friday', None, 'mdi:timer', 1], + 'Zone1TimerSaturday': + ['z1Timer.Saturday', None, 'mdi:timer', 1], + 'Zone1TimerSunday': + ['z1Timer.Sunday', None, 'mdi:timer', 1], + 'Zone1OperativeMode': + ['z1OpMode', None, 'mdi:math-compass', 3], + 'ContinuosHeating': + ['ContinuosHeating', '°C', 'mdi:weather-snowy', 0], + 'PowerEnergyConsumptionLastMonth': + ['PrEnergySumHcLastMonth', 'kWh', 'mdi:flash', 0], + 'PowerEnergyConsumptionThisMonth': + ['PrEnergySumHcThisMonth', 'kWh', 'mdi:flash', 0] + }, + 'ehp': { + 'HWTemperature': + ['HwcTemp', '°C', 'mdi:thermometer', 4], + 'OutsideTemp': + ['OutsideTemp', '°C', 'mdi:thermometer', 4] + }, + 'bai': { + 'ReturnTemperature': + ['ReturnTemp', '°C', 'mdi:thermometer', 4], + 'CentralHeatingPump': + ['WP', None, 'mdi:toggle-switch', 2], + 'HeatingSwitch': + ['HeatingSwitch', None, 'mdi:toggle-switch', 2], + 'FlowTemperature': + ['FlowTemp', '°C', 'mdi:thermometer', 4], + 'Flame': + ['Flame', None, 'mdi:toggle-switch', 2], + 'PowerEnergyConsumptionHeatingCircuit': + ['PrEnergySumHc1', 'kWh', 'mdi:flash', 0], + 'PowerEnergyConsumptionHotWaterCircuit': + ['PrEnergySumHwc1', 'kWh', 'mdi:flash', 0], + 'RoomThermostat': + ['DCRoomthermostat', None, 'mdi:toggle-switch', 2], + 'HeatingPartLoad': + ['PartloadHcKW', 'kWh', 'mdi:flash', 0] + } +} diff --git a/homeassistant/components/ebusd/sensor.py b/homeassistant/components/ebusd/sensor.py new file mode 100644 index 00000000000..24ca263898c --- /dev/null +++ b/homeassistant/components/ebusd/sensor.py @@ -0,0 +1,105 @@ +""" +Support for Ebusd daemon for communication with eBUS heating systems. + +For more details about ebusd deamon, please refer to the documentation at +https://github.com/john30/ebusd +""" + +import logging +import datetime + +from homeassistant.helpers.entity import Entity + +from .const import DOMAIN + +DEPENDENCIES = ['ebusd'] + +TIME_FRAME1_BEGIN = 'time_frame1_begin' +TIME_FRAME1_END = 'time_frame1_end' +TIME_FRAME2_BEGIN = 'time_frame2_begin' +TIME_FRAME2_END = 'time_frame2_end' +TIME_FRAME3_BEGIN = 'time_frame3_begin' +TIME_FRAME3_END = 'time_frame3_end' + +_LOGGER = logging.getLogger(__name__) + + +def setup_platform(hass, config, add_entities, discovery_info=None): + """Set up the Ebus sensor.""" + ebusd_api = hass.data[DOMAIN] + monitored_conditions = discovery_info['monitored_conditions'] + name = discovery_info['client_name'] + + dev = [] + for condition in monitored_conditions: + dev.append(EbusdSensor( + ebusd_api, discovery_info['sensor_types'][condition], name)) + + add_entities(dev, True) + + +class EbusdSensor(Entity): + """Ebusd component sensor methods definition.""" + + def __init__(self, data, sensor, name): + """Initialize the sensor.""" + self._state = None + self._client_name = name + self._name, self._unit_of_measurement, self._icon, self._type = sensor + self.data = data + + @property + def name(self): + """Return the name of the sensor.""" + return '{} {}'.format(self._client_name, self._name) + + @property + def state(self): + """Return the state of the sensor.""" + return self._state + + @property + def device_state_attributes(self): + """Return the device state attributes.""" + if self._type == 1 and self._state is not None: + schedule = { + TIME_FRAME1_BEGIN: None, + TIME_FRAME1_END: None, + TIME_FRAME2_BEGIN: None, + TIME_FRAME2_END: None, + TIME_FRAME3_BEGIN: None, + TIME_FRAME3_END: None + } + time_frame = self._state.split(';') + for index, item in enumerate(sorted(schedule.items())): + if index < len(time_frame): + parsed = datetime.datetime.strptime( + time_frame[index], '%H:%M') + parsed = parsed.replace( + datetime.datetime.now().year, + datetime.datetime.now().month, + datetime.datetime.now().day) + schedule[item[0]] = parsed.isoformat() + return schedule + return None + + @property + def icon(self): + """Icon to use in the frontend, if any.""" + return self._icon + + @property + def unit_of_measurement(self): + """Return the unit of measurement.""" + return self._unit_of_measurement + + def update(self): + """Fetch new state data for the sensor.""" + try: + self.data.update(self._name, self._type) + if self._name not in self.data.value: + return + + self._state = self.data.value[self._name] + except RuntimeError: + _LOGGER.debug("EbusdData.update exception") diff --git a/homeassistant/components/ebusd/services.yaml b/homeassistant/components/ebusd/services.yaml new file mode 100644 index 00000000000..0f64533f7f1 --- /dev/null +++ b/homeassistant/components/ebusd/services.yaml @@ -0,0 +1,6 @@ +write: + description: Call ebusd write command. + fields: + call: + description: Property name and value to set + example: '{"name": "Hc1MaxFlowTempDesired", "value": 21}' \ No newline at end of file diff --git a/homeassistant/components/ebusd/strings.json b/homeassistant/components/ebusd/strings.json new file mode 100644 index 00000000000..ee62df8ddad --- /dev/null +++ b/homeassistant/components/ebusd/strings.json @@ -0,0 +1,6 @@ +{ + "state": { + "day": "Day", + "night": "Night" + } +} \ No newline at end of file diff --git a/requirements_all.txt b/requirements_all.txt index 3d6a40a31b3..a025998e877 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -348,6 +348,9 @@ dsmr_parser==0.12 # homeassistant.components.dweet.sensor dweepy==0.3.0 +# homeassistant.components.ebusd +ebusdpy==0.0.16 + # homeassistant.components.ecoal_boiler ecoaliface==0.4.0