Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 4 additions & 1 deletion .coveragerc
Original file line number Diff line number Diff line change
Expand Up @@ -461,7 +461,10 @@ omit =
homeassistant/components/openhome/media_player.py
homeassistant/components/opensensemap/air_quality.py
homeassistant/components/opensky/sensor.py
homeassistant/components/opentherm_gw/*
homeassistant/components/opentherm_gw/__init__.py
homeassistant/components/opentherm_gw/binary_sensor.py
homeassistant/components/opentherm_gw/climate.py
homeassistant/components/opentherm_gw/sensor.py
homeassistant/components/openuv/__init__.py
homeassistant/components/openuv/binary_sensor.py
homeassistant/components/openuv/sensor.py
Expand Down
23 changes: 23 additions & 0 deletions homeassistant/components/opentherm_gw/.translations/en.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"config": {
"error": {
"already_configured": "Gateway already configured",
"id_exists": "Gateway id already exists",
"serial_error": "Error connecting to device",
"timeout": "Connection attempt timed out"
},
"step": {
"init": {
"data": {
"device": "Path or URL",
"floor_temperature": "Floor climate temperature",
"id": "ID",
"name": "Name",
"precision": "Climate temperature precision"
},
"title": "OpenTherm Gateway"
}
},
"title": "OpenTherm Gateway"
}
}
23 changes: 23 additions & 0 deletions homeassistant/components/opentherm_gw/.translations/nl.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
{
"config": {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

We don't add other translations in the config flow PR. Just the default English translation.

Translations are handled separately via Lokalise and synced regularly to github.

https://developers.home-assistant.io/docs/en/internationalization_backend_localization.html#translation-strings

"error": {
"already_configured": "Gateway is reeds geconfigureerd",
"id_exists": "Gateway id bestaat reeds",
"serial_error": "Kan niet verbinden met de Gateway",
"timeout": "Time-out van de verbinding"
},
"step": {
"init": {
"data": {
"device": "Pad of URL",
"floor_temperature": "Thermostaat temperaturen naar beneden afronden",
"id": "ID",
"name": "Naam",
"precision": "Thermostaat temperatuur precisie"
},
"title": "OpenTherm Gateway"
}
},
"title": "OpenTherm Gateway"
}
}
66 changes: 40 additions & 26 deletions homeassistant/components/opentherm_gw/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import pyotgw.vars as gw_vars
import voluptuous as vol

from homeassistant.config_entries import SOURCE_IMPORT
from homeassistant.components.binary_sensor import DOMAIN as COMP_BINARY_SENSOR
from homeassistant.components.climate import DOMAIN as COMP_CLIMATE
from homeassistant.components.sensor import DOMAIN as COMP_SENSOR
Expand All @@ -16,13 +17,13 @@
ATTR_TEMPERATURE,
ATTR_TIME,
CONF_DEVICE,
CONF_ID,
CONF_NAME,
EVENT_HOMEASSISTANT_STOP,
PRECISION_HALVES,
PRECISION_TENTHS,
PRECISION_WHOLE,
)
from homeassistant.helpers.discovery import async_load_platform
from homeassistant.helpers.dispatcher import async_dispatcher_send

import homeassistant.helpers.config_validation as cv
Expand All @@ -36,6 +37,7 @@
CONF_PRECISION,
DATA_GATEWAYS,
DATA_OPENTHERM_GW,
DOMAIN,
SERVICE_RESET_GATEWAY,
SERVICE_SET_CLOCK,
SERVICE_SET_CONTROL_SETPOINT,
Expand All @@ -50,8 +52,6 @@

_LOGGER = logging.getLogger(__name__)

DOMAIN = "opentherm_gw"

CLIMATE_SCHEMA = vol.Schema(
{
vol.Optional(CONF_PRECISION): vol.In(
Expand All @@ -75,28 +75,41 @@
)


async def async_setup(hass, config):
async def async_setup_entry(hass, config_entry):
"""Set up the OpenTherm Gateway component."""
conf = config[DOMAIN]
hass.data[DATA_OPENTHERM_GW] = {DATA_GATEWAYS: {}}
for gw_id, cfg in conf.items():
gateway = OpenThermGatewayDevice(hass, gw_id, cfg)
hass.data[DATA_OPENTHERM_GW][DATA_GATEWAYS][gw_id] = gateway
hass.async_create_task(
async_load_platform(hass, COMP_CLIMATE, DOMAIN, gw_id, config)
)
hass.async_create_task(
async_load_platform(hass, COMP_BINARY_SENSOR, DOMAIN, gw_id, config)
)
if DATA_OPENTHERM_GW not in hass.data:
hass.data[DATA_OPENTHERM_GW] = {DATA_GATEWAYS: {}}

gateway = OpenThermGatewayDevice(hass, config_entry)
hass.data[DATA_OPENTHERM_GW][DATA_GATEWAYS][config_entry.data[CONF_ID]] = gateway

# Schedule directly on the loop to avoid blocking HA startup.
hass.loop.create_task(gateway.connect_and_subscribe())

for comp in [COMP_BINARY_SENSOR, COMP_CLIMATE, COMP_SENSOR]:
hass.async_create_task(
async_load_platform(hass, COMP_SENSOR, DOMAIN, gw_id, config)
hass.config_entries.async_forward_entry_setup(config_entry, comp)
)
# Schedule directly on the loop to avoid blocking HA startup.
hass.loop.create_task(gateway.connect_and_subscribe(cfg[CONF_DEVICE]))

register_services(hass)
return True


async def async_setup(hass, config):
"""Set up the OpenTherm Gateway component."""
if not hass.config_entries.async_entries(DOMAIN) and DOMAIN in config:
conf = config[DOMAIN]
for device_id, device_config in conf.items():
device_config[CONF_ID] = device_id

hass.async_create_task(
hass.config_entries.flow.async_init(
DOMAIN, context={"source": SOURCE_IMPORT}, data=device_config
)
)
return True


def register_services(hass):
"""Register services for the component."""
service_reset_schema = vol.Schema(
Expand Down Expand Up @@ -326,20 +339,21 @@ async def set_setback_temp(call):
class OpenThermGatewayDevice:
"""OpenTherm Gateway device class."""

def __init__(self, hass, gw_id, config):
def __init__(self, hass, config_entry):
"""Initialize the OpenTherm Gateway."""
self.hass = hass
self.gw_id = gw_id
self.name = config.get(CONF_NAME, gw_id)
self.climate_config = config[CONF_CLIMATE]
self.device_path = config_entry.data[CONF_DEVICE]
self.gw_id = config_entry.data[CONF_ID]
self.name = config_entry.data[CONF_NAME]
self.climate_config = config_entry.options
self.status = {}
self.update_signal = f"{DATA_OPENTHERM_GW}_{gw_id}_update"
self.update_signal = f"{DATA_OPENTHERM_GW}_{self.gw_id}_update"
self.gateway = pyotgw.pyotgw()

async def connect_and_subscribe(self, device_path):
async def connect_and_subscribe(self):
"""Connect to serial device and subscribe report handler."""
await self.gateway.connect(self.hass.loop, device_path)
_LOGGER.debug("Connected to OpenTherm Gateway at %s", device_path)
await self.gateway.connect(self.hass.loop, self.device_path)
_LOGGER.debug("Connected to OpenTherm Gateway at %s", self.device_path)

async def cleanup(event):
"""Reset overrides on the gateway."""
Expand Down
14 changes: 9 additions & 5 deletions homeassistant/components/opentherm_gw/binary_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import logging

from homeassistant.components.binary_sensor import ENTITY_ID_FORMAT, BinarySensorDevice
from homeassistant.const import CONF_ID
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity import async_generate_entity_id
Expand All @@ -12,18 +13,21 @@
_LOGGER = logging.getLogger(__name__)


async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up the OpenTherm Gateway binary sensors."""
if discovery_info is None:
return
gw_dev = hass.data[DATA_OPENTHERM_GW][DATA_GATEWAYS][discovery_info]
sensors = []
for var, info in BINARY_SENSOR_INFO.items():
device_class = info[0]
friendly_name_format = info[1]
sensors.append(
OpenThermBinarySensor(gw_dev, var, device_class, friendly_name_format)
OpenThermBinarySensor(
hass.data[DATA_OPENTHERM_GW][DATA_GATEWAYS][config_entry.data[CONF_ID]],
var,
device_class,
friendly_name_format,
)
)

async_add_entities(sensors)


Expand Down
19 changes: 12 additions & 7 deletions homeassistant/components/opentherm_gw/climate.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
)
from homeassistant.const import (
ATTR_TEMPERATURE,
CONF_ID,
PRECISION_HALVES,
PRECISION_TENTHS,
PRECISION_WHOLE,
Expand All @@ -33,12 +34,16 @@
SUPPORT_FLAGS = SUPPORT_TARGET_TEMPERATURE | SUPPORT_PRESET_MODE


async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Set up the opentherm_gw device."""
gw_dev = hass.data[DATA_OPENTHERM_GW][DATA_GATEWAYS][discovery_info]
async def async_setup_entry(hass, config_entry, async_add_entities):
"""Set up an OpenTherm Gateway climate entity."""
ents = []
ents.append(
OpenThermClimate(
hass.data[DATA_OPENTHERM_GW][DATA_GATEWAYS][config_entry.data[CONF_ID]]
)
)

gateway = OpenThermClimate(gw_dev)
async_add_entities([gateway])
async_add_entities(ents)


class OpenThermClimate(ClimateDevice):
Expand All @@ -48,7 +53,7 @@ def __init__(self, gw_dev):
"""Initialize the device."""
self._gateway = gw_dev
self.friendly_name = gw_dev.name
self.floor_temp = gw_dev.climate_config[CONF_FLOOR_TEMP]
self.floor_temp = gw_dev.climate_config.get(CONF_FLOOR_TEMP)
self.temp_precision = gw_dev.climate_config.get(CONF_PRECISION)
self._current_operation = None
self._current_temperature = None
Expand All @@ -62,7 +67,7 @@ def __init__(self, gw_dev):

async def async_added_to_hass(self):
"""Connect to the OpenTherm Gateway device."""
_LOGGER.debug("Added device %s", self.friendly_name)
_LOGGER.debug("Added OpenTherm Gateway climate device %s", self.friendly_name)
async_dispatcher_connect(
self.hass, self._gateway.update_signal, self.receive_report
)
Expand Down
91 changes: 91 additions & 0 deletions homeassistant/components/opentherm_gw/config_flow.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
"""OpenTherm Gateway config flow."""
import asyncio
from serial import SerialException

import pyotgw
import voluptuous as vol

from homeassistant import config_entries
from homeassistant.const import CONF_DEVICE, CONF_ID, CONF_NAME

import homeassistant.helpers.config_validation as cv

from . import DOMAIN


class OpenThermGwConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""OpenTherm Gateway Config Flow."""

VERSION = 1
CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH

async def async_step_init(self, info=None):
"""Handle config flow initiation."""
if info:
name = info[CONF_NAME]
device = info[CONF_DEVICE]
gw_id = cv.slugify(info.get(CONF_ID, name))

entries = [e.data for e in self.hass.config_entries.async_entries(DOMAIN)]

if gw_id in [e[CONF_ID] for e in entries]:
return self._show_form({"base": "id_exists"})

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

If we use the CONF_ID key instead of "base" the form will show the error message next to the corresponding form item.


if device in [e[CONF_DEVICE] for e in entries]:
return self._show_form({"base": "already_configured"})

async def test_connection():
"""Try to connect to the OpenTherm Gateway."""
otgw = pyotgw.pyotgw()
status = await otgw.connect(self.hass.loop, device)
await otgw.disconnect()
return status.get(pyotgw.OTGW_ABOUT)

try:
res = await asyncio.wait_for(test_connection(), timeout=10)
except asyncio.TimeoutError:
return self._show_form({"base": "timeout"})
except SerialException:
return self._show_form({"base": "serial_error"})

if res:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

When is this not true?

return self._create_entry(gw_id, name, device)

return self._show_form()

async def async_step_user(self, info=None):
"""Handle manual initiation of the config flow."""
return await self.async_step_init(info)

async def async_step_import(self, import_config):
"""
Import an OpenTherm Gateway device as a config entry.

This flow is triggered by `async_setup` for configured devices.
"""
formatted_config = {
CONF_NAME: import_config.get(CONF_NAME, import_config[CONF_ID]),
CONF_DEVICE: import_config[CONF_DEVICE],
CONF_ID: import_config[CONF_ID],
}
return await self.async_step_init(info=formatted_config)

def _show_form(self, errors=None):
"""Show the config flow form with possible errors."""
return self.async_show_form(
step_id="init",
data_schema=vol.Schema(
{
vol.Required(CONF_NAME): str,
vol.Required(CONF_DEVICE): str,
vol.Optional(CONF_ID): str,
}
),
errors=errors or {},
)

def _create_entry(self, gw_id, name, device):
"""Create entry for the OpenTherm Gateway device."""
return self.async_create_entry(
title=name, data={CONF_ID: gw_id, CONF_DEVICE: device, CONF_NAME: name}
)
2 changes: 2 additions & 0 deletions homeassistant/components/opentherm_gw/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@
DEVICE_CLASS_HEAT = "heat"
DEVICE_CLASS_PROBLEM = "problem"

DOMAIN = "opentherm_gw"

SERVICE_RESET_GATEWAY = "reset_gateway"
SERVICE_SET_CLOCK = "set_clock"
SERVICE_SET_CONTROL_SETPOINT = "set_control_setpoint"
Expand Down
7 changes: 4 additions & 3 deletions homeassistant/components/opentherm_gw/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@
"name": "Opentherm Gateway",
"documentation": "https://www.home-assistant.io/integrations/opentherm_gw",
"requirements": [
"pyotgw==0.4b4"
"pyotgw==0.5b0"
],
"dependencies": [],
"codeowners": [
"@mvn23"
]
}
],
"config_flow": true
}
Loading