diff --git a/.coveragerc b/.coveragerc index a935564791a7..12b46b017843 100644 --- a/.coveragerc +++ b/.coveragerc @@ -93,6 +93,9 @@ omit = homeassistant/components/ecobee.py homeassistant/components/*/ecobee.py + homeassistant/components/ebus/* + homeassistant/components/*/ebusd.py + homeassistant/components/edp_redy.py homeassistant/components/*/edp_redy.py diff --git a/homeassistant/components/ebus/.translations/ebusd.en.json b/homeassistant/components/ebus/.translations/ebusd.en.json new file mode 100644 index 000000000000..78260881618a --- /dev/null +++ b/homeassistant/components/ebus/.translations/ebusd.en.json @@ -0,0 +1,7 @@ +{ + "state": { + "auto": "Automatic", + "day": "Day", + "night": "Night" + } +} \ No newline at end of file diff --git a/homeassistant/components/ebus/.translations/ebusd.it.json b/homeassistant/components/ebus/.translations/ebusd.it.json new file mode 100644 index 000000000000..d65493973eaa --- /dev/null +++ b/homeassistant/components/ebus/.translations/ebusd.it.json @@ -0,0 +1,7 @@ +{ + "state": { + "auto": "Automatico", + "day": "Giorno", + "night": "Notte" + } +} \ No newline at end of file diff --git a/homeassistant/components/ebus/__init__.py b/homeassistant/components/ebus/__init__.py new file mode 100644 index 000000000000..9810f9e4623c --- /dev/null +++ b/homeassistant/components/ebus/__init__.py @@ -0,0 +1,38 @@ +""" +Provides functionality to interact with ebus devices. + +For more details about this component, please refer to the documentation at +https://home-assistant.io/components/ebus/ +""" +from datetime import timedelta +import logging + +from homeassistant.helpers.entity_component import EntityComponent + +DOMAIN = 'ebus' + +ENTITY_ID_FORMAT = DOMAIN + '.{}' + +SCAN_INTERVAL = timedelta(seconds=10) +MIN_TIME_BETWEEN_UPDATES = timedelta(seconds=15) + +_LOGGER = logging.getLogger(__name__) + + +async def async_setup(hass, config): + """Set up climate devices.""" + component = hass.data[DOMAIN] = \ + EntityComponent(_LOGGER, DOMAIN, hass, SCAN_INTERVAL) + await component.async_setup(config) + + return True + + +async def async_setup_entry(hass, entry): + """Set up a config entry.""" + return await hass.data[DOMAIN].async_setup_entry(entry) + + +async def async_unload_entry(hass, entry): + """Unload a config entry.""" + return await hass.data[DOMAIN].async_unload_entry(entry) diff --git a/homeassistant/components/ebus/const.py b/homeassistant/components/ebus/const.py new file mode 100644 index 000000000000..5eb0f76e3e82 --- /dev/null +++ b/homeassistant/components/ebus/const.py @@ -0,0 +1,84 @@ +"""Constants for ebus component.""" +DOMAIN = 'ebus' + +READ_COMMAND = 'read -m {2} -c {0} {1}\n' +WRITE_COMMAND = 'write -c {0} {1} {2}\n' + +""" + 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] + } +} diff --git a/homeassistant/components/ebus/ebusd.py b/homeassistant/components/ebus/ebusd.py new file mode 100644 index 000000000000..a973483f7abb --- /dev/null +++ b/homeassistant/components/ebus/ebusd.py @@ -0,0 +1,189 @@ +""" +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 +""" + +from datetime import timedelta +import logging +import socket + +import voluptuous as vol + +from homeassistant.components.sensor import PLATFORM_SCHEMA +from homeassistant.const import ( + CONF_NAME, CONF_HOST, CONF_PORT, CONF_MONITORED_CONDITIONS, + STATE_ON, STATE_OFF) +from homeassistant.exceptions import PlatformNotReady +import homeassistant.helpers.config_validation as cv +from homeassistant.helpers.entity import Entity +from homeassistant.util import Throttle + +from .const import ( + DOMAIN, SENSOR_TYPES, READ_COMMAND, WRITE_COMMAND) + +REQUIREMENTS = ['ebusdpy==0.0.4'] + +_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) + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + 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)]) +}) + + +def setup_platform(hass, config, add_devices, discovery_info=None): + """Set up the Ebusd...""" + name = config.get(CONF_NAME) + circuit = config.get(CONF_CIRCUIT) + server_address = (config.get(CONF_HOST), config.get(CONF_PORT)) + + try: + sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + data = EbusdData(server_address, circuit) + + sock.settimeout(5) + sock.connect(server_address) + sock.close() + + dev = [] + for variable in config[CONF_MONITORED_CONDITIONS]: + dev.append(Ebusd(data, circuit, variable, name)) + + add_devices(dev) + hass.services.register(DOMAIN, SERVICE_EBUSD_WRITE, data.write) + except socket.timeout: + raise PlatformNotReady + except socket.error: + raise PlatformNotReady + + +def timer_format(string): + """Datetime formatter.""" + _r = [] + _s = string.split(';') + for i in range(0, len(_s) // 2): + if(_s[i * 2] != '-:-' and _s[i * 2] != _s[(i * 2) + 1]): + _r.append(_s[i * 2] + '/' + _s[(i * 2) + 1]) + return ' - '.join(_r) + + +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): + """Call the Ebusd API to update the data.""" + import ebusdpy + command = READ_COMMAND.format(self._circuit, name, CACHE_TTL) + + try: + _LOGGER.debug("Opening socket to ebusd %s: %s", name, command) + command_result = ebusdpy.send_command(self._address, command) + if 'not found' in command_result: + _LOGGER.warning("Element not found: %s", name) + raise RuntimeError("Element not found") + else: + self.value[name] = command_result + except socket.timeout: + _LOGGER.error("socket timeout error") + raise RuntimeError("socket timeout") + except socket.error: + _LOGGER.error("socket error: %s", socket.error) + raise RuntimeError("Command failed") + + def write(self, call): + """Call write methon on ebusd.""" + import ebusdpy + name = call.data.get('name') + value = call.data.get('value') + command = WRITE_COMMAND.format(self._circuit, name, value) + + try: + _LOGGER.debug("Opening socket to ebusd %s: %s", name, command) + command_result = ebusdpy.send_command(self._address, command) + if 'done' not in command_result: + _LOGGER.warning('Write command failed: %s', name) + except socket.timeout: + _LOGGER.error("socket timeout error") + except socket.error: + _LOGGER.error() + + +class Ebusd(Entity): + """Representation of a Sensor.""" + + def __init__(self, data, circuit, sensor_type, name): + """Initialize the sensor.""" + self._state = None + self._client_name = name + self._name = SENSOR_TYPES[circuit][sensor_type][0] + self._unit_of_measurement = SENSOR_TYPES[circuit][sensor_type][1] + self._icon = SENSOR_TYPES[circuit][sensor_type][2] + self._type = SENSOR_TYPES[circuit][sensor_type][3] + 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 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) + if self._name not in self.data.value: + return + + if self._type == 0: + self._state = format( + float(self.data.value[self._name]), '.1f') + elif self._type == 1: + self._state = timer_format(self.data.value[self._name]) + elif self._type == 2: + if self.data.value[self._name] == 1: + self._state = STATE_ON + else: + self._state = STATE_OFF + elif self._type == 3: + self._state = self.data.value[self._name] + elif self._type == 4: + if 'ok' not in self.data.value[self._name].split(';'): + return + self._state = self.data.value[self._name].partition(';')[0] + except RuntimeError: + _LOGGER.debug("EbusdData.update exception") diff --git a/homeassistant/components/ebus/services.yaml b/homeassistant/components/ebus/services.yaml new file mode 100644 index 000000000000..0f64533f7f15 --- /dev/null +++ b/homeassistant/components/ebus/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/ebus/strings.json b/homeassistant/components/ebus/strings.json new file mode 100644 index 000000000000..78260881618a --- /dev/null +++ b/homeassistant/components/ebus/strings.json @@ -0,0 +1,7 @@ +{ + "state": { + "auto": "Automatic", + "day": "Day", + "night": "Night" + } +} \ No newline at end of file diff --git a/requirements_all.txt b/requirements_all.txt index 4600af0c5426..377b28aaf13f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -308,6 +308,9 @@ dsmr_parser==0.11 # homeassistant.components.sensor.dweet dweepy==0.3.0 +# homeassistant.components.ebus.ebusd +ebusdpy==0.0.4 + # homeassistant.components.edp_redy edp_redy==0.0.2