Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ebusd integration #16899

Closed
wants to merge 21 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
3 changes: 3 additions & 0 deletions .coveragerc
Expand Up @@ -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

Expand Down
7 changes: 7 additions & 0 deletions homeassistant/components/ebus/.translations/ebusd.en.json
@@ -0,0 +1,7 @@
{
"state": {
"auto": "Automatic",
"day": "Day",
"night": "Night"
}
}
7 changes: 7 additions & 0 deletions homeassistant/components/ebus/.translations/ebusd.it.json
@@ -0,0 +1,7 @@
{
"state": {
"auto": "Automatico",
"day": "Giorno",
"night": "Notte"
}
}
38 changes: 38 additions & 0 deletions 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):
CrazYoshi marked this conversation as resolved.
Show resolved Hide resolved
"""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)
84 changes: 84 additions & 0 deletions 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':
CrazYoshi marked this conversation as resolved.
Show resolved Hide resolved
['HwcTemp', '°C', 'mdi:thermometer', 4],
'OutsideTemp':
['OutsideTemp', '°C', 'mdi:thermometer', 4]
}
}
189 changes: 189 additions & 0 deletions 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 (
CrazYoshi marked this conversation as resolved.
Show resolved Hide resolved
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):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are we adding a sensor entity to a ebus component platform? Shouldn't this be a sensor component platform?

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I believe that the author tried to pull this as a sensor before, but @balloob has rejected that PR asking CrazYoshi to rewrite this as a component.
Also there's a huge potential for this component to grow in the future, right now it's mostly reading sensor values, but in the future it may also get binary sensors, switches, or even thermostat functionality.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes exactly, if you scroll up you can see the change requested by him. That's the reason why I start a new component. Ebus is a generic container where also other component using the same standard can be put inside.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

even though you have a component, you can still have the sensor be defined in sensor/ebusd.py

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So what should I do to this component again? I didn't get
Can you give me some suggestions?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Component manages connection and hosts services, sensor platform of ebusd will show some info as a sensor 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:
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would also add the recognition of such a value as 'on' or self.data.value[self._name] == 'on' , , because frequent are just such answers

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there is the STATE_ON for that. In this way it will use also translation

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")
6 changes: 6 additions & 0 deletions 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}'
7 changes: 7 additions & 0 deletions homeassistant/components/ebus/strings.json
@@ -0,0 +1,7 @@
{
"state": {
"auto": "Automatic",
"day": "Day",
"night": "Night"
}
}
3 changes: 3 additions & 0 deletions requirements_all.txt
Expand Up @@ -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

Expand Down