1 """Support for OpenTherm Gateway devices."""
4 from datetime
import date, datetime
7 from pyotgw
import OpenThermGateway
8 import pyotgw.vars
as gw_vars
9 from serial
import SerialException
10 import voluptuous
as vol
23 EVENT_HOMEASSISTANT_STOP,
32 config_validation
as cv,
33 device_registry
as dr,
34 entity_registry
as er,
54 SERVICE_RESET_GATEWAY,
55 SERVICE_SEND_TRANSP_CMD,
58 SERVICE_SET_CONTROL_SETPOINT,
59 SERVICE_SET_GPIO_MODE,
60 SERVICE_SET_HOT_WATER_OVRD,
61 SERVICE_SET_HOT_WATER_SETPOINT,
67 OpenThermDeviceIdentifier,
70 _LOGGER = logging.getLogger(__name__)
73 CLIMATE_SCHEMA = vol.Schema(
75 vol.Optional(CONF_PRECISION): vol.In(
76 [PRECISION_TENTHS, PRECISION_HALVES, PRECISION_WHOLE]
78 vol.Optional(CONF_FLOOR_TEMP, default=
False): cv.boolean,
82 CONFIG_SCHEMA = vol.Schema(
84 DOMAIN: cv.schema_with_slug_keys(
86 vol.Required(CONF_DEVICE): cv.string,
87 vol.Optional(CONF_CLIMATE, default={}): CLIMATE_SCHEMA,
88 vol.Optional(CONF_NAME): cv.string,
92 extra=vol.ALLOW_EXTRA,
96 Platform.BINARY_SENSOR,
106 """Handle options update."""
107 gateway = hass.data[DATA_OPENTHERM_GW][DATA_GATEWAYS][entry.data[CONF_ID]]
112 """Set up the OpenTherm Gateway component."""
113 if DATA_OPENTHERM_GW
not in hass.data:
114 hass.data[DATA_OPENTHERM_GW] = {DATA_GATEWAYS: {}}
117 hass.data[DATA_OPENTHERM_GW][DATA_GATEWAYS][config_entry.data[CONF_ID]] = gateway
120 dev_reg = dr.async_get(hass)
122 migrate_device := dev_reg.async_get_device(
123 {(DOMAIN, config_entry.data[CONF_ID])}
126 dev_reg.async_update_device(
131 f
"{config_entry.data[CONF_ID]}-{OpenThermDeviceIdentifier.GATEWAY}",
137 ent_reg = er.async_get(hass)
139 entity_id := ent_reg.async_get_entity_id(
140 CLIMATE_DOMAIN, DOMAIN, config_entry.data[CONF_ID]
143 ent_reg.async_update_entity(
145 new_unique_id=f
"{config_entry.data[CONF_ID]}-{OpenThermDeviceIdentifier.THERMOSTAT}-thermostat_entity",
148 config_entry.add_update_listener(options_updated)
151 async
with asyncio.timeout(CONNECTION_TIMEOUT):
152 await gateway.connect_and_subscribe()
153 except (TimeoutError, ConnectionError, SerialException)
as ex:
154 await gateway.cleanup()
156 f
"Could not connect to gateway at {gateway.device_path}: {ex}"
159 await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS)
166 async
def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
167 """Set up the OpenTherm Gateway component."""
169 ir.async_create_issue(
172 "deprecated_import_from_configuration_yaml",
173 breaks_in_ha_version=
"2025.4.0",
176 severity=ir.IssueSeverity.WARNING,
177 translation_key=
"deprecated_import_from_configuration_yaml",
179 if not hass.config_entries.async_entries(DOMAIN)
and DOMAIN
in config:
180 conf = config[DOMAIN]
181 for device_id, device_config
in conf.items():
182 device_config[CONF_ID] = device_id
184 hass.async_create_task(
185 hass.config_entries.flow.async_init(
186 DOMAIN, context={
"source": SOURCE_IMPORT}, data=device_config
193 """Register services for the component."""
194 service_reset_schema = vol.Schema(
196 vol.Required(ATTR_GW_ID): vol.All(
197 cv.string, vol.In(hass.data[DATA_OPENTHERM_GW][DATA_GATEWAYS])
201 service_set_central_heating_ovrd_schema = vol.Schema(
203 vol.Required(ATTR_GW_ID): vol.All(
204 cv.string, vol.In(hass.data[DATA_OPENTHERM_GW][DATA_GATEWAYS])
206 vol.Required(ATTR_CH_OVRD): cv.boolean,
209 service_set_clock_schema = vol.Schema(
211 vol.Required(ATTR_GW_ID): vol.All(
212 cv.string, vol.In(hass.data[DATA_OPENTHERM_GW][DATA_GATEWAYS])
214 vol.Optional(ATTR_DATE, default=date.today): cv.date,
215 vol.Optional(ATTR_TIME, default=
lambda: datetime.now().
time()): cv.time,
218 service_set_control_setpoint_schema = vol.Schema(
220 vol.Required(ATTR_GW_ID): vol.All(
221 cv.string, vol.In(hass.data[DATA_OPENTHERM_GW][DATA_GATEWAYS])
223 vol.Required(ATTR_TEMPERATURE): vol.All(
224 vol.Coerce(float), vol.Range(min=0, max=90)
228 service_set_hot_water_setpoint_schema = service_set_control_setpoint_schema
229 service_set_hot_water_ovrd_schema = vol.Schema(
231 vol.Required(ATTR_GW_ID): vol.All(
232 cv.string, vol.In(hass.data[DATA_OPENTHERM_GW][DATA_GATEWAYS])
234 vol.Required(ATTR_DHW_OVRD): vol.Any(
235 vol.Equal(
"A"), vol.All(vol.Coerce(int), vol.Range(min=0, max=1))
239 service_set_gpio_mode_schema = vol.Schema(
243 vol.Required(ATTR_GW_ID): vol.All(
244 cv.string, vol.In(hass.data[DATA_OPENTHERM_GW][DATA_GATEWAYS])
246 vol.Required(ATTR_ID): vol.Equal(
"A"),
247 vol.Required(ATTR_MODE): vol.All(
248 vol.Coerce(int), vol.Range(min=0, max=6)
254 vol.Required(ATTR_GW_ID): vol.All(
255 cv.string, vol.In(hass.data[DATA_OPENTHERM_GW][DATA_GATEWAYS])
257 vol.Required(ATTR_ID): vol.Equal(
"B"),
258 vol.Required(ATTR_MODE): vol.All(
259 vol.Coerce(int), vol.Range(min=0, max=7)
265 service_set_led_mode_schema = vol.Schema(
267 vol.Required(ATTR_GW_ID): vol.All(
268 cv.string, vol.In(hass.data[DATA_OPENTHERM_GW][DATA_GATEWAYS])
270 vol.Required(ATTR_ID): vol.In(
"ABCDEF"),
271 vol.Required(ATTR_MODE): vol.In(
"RXTBOFHWCEMP"),
274 service_set_max_mod_schema = vol.Schema(
276 vol.Required(ATTR_GW_ID): vol.All(
277 cv.string, vol.In(hass.data[DATA_OPENTHERM_GW][DATA_GATEWAYS])
279 vol.Required(ATTR_LEVEL): vol.All(
280 vol.Coerce(int), vol.Range(min=-1, max=100)
284 service_set_oat_schema = vol.Schema(
286 vol.Required(ATTR_GW_ID): vol.All(
287 cv.string, vol.In(hass.data[DATA_OPENTHERM_GW][DATA_GATEWAYS])
289 vol.Required(ATTR_TEMPERATURE): vol.All(
290 vol.Coerce(float), vol.Range(min=-40, max=99)
294 service_set_sb_temp_schema = vol.Schema(
296 vol.Required(ATTR_GW_ID): vol.All(
297 cv.string, vol.In(hass.data[DATA_OPENTHERM_GW][DATA_GATEWAYS])
299 vol.Required(ATTR_TEMPERATURE): vol.All(
300 vol.Coerce(float), vol.Range(min=0, max=30)
304 service_send_transp_cmd_schema = vol.Schema(
306 vol.Required(ATTR_GW_ID): vol.All(
307 cv.string, vol.In(hass.data[DATA_OPENTHERM_GW][DATA_GATEWAYS])
309 vol.Required(ATTR_TRANSP_CMD): vol.All(
310 cv.string, vol.Length(min=2, max=2), vol.Coerce(str.upper)
312 vol.Required(ATTR_TRANSP_ARG): vol.All(
313 cv.string, vol.Length(min=1, max=12)
318 async
def reset_gateway(call: ServiceCall) ->
None:
319 """Reset the OpenTherm Gateway."""
320 gw_hub = hass.data[DATA_OPENTHERM_GW][DATA_GATEWAYS][call.data[ATTR_GW_ID]]
321 mode_rst = gw_vars.OTGW_MODE_RESET
322 await gw_hub.gateway.set_mode(mode_rst)
324 hass.services.async_register(
325 DOMAIN, SERVICE_RESET_GATEWAY, reset_gateway, service_reset_schema
328 async
def set_ch_ovrd(call: ServiceCall) ->
None:
329 """Set the central heating override on the OpenTherm Gateway."""
330 gw_hub = hass.data[DATA_OPENTHERM_GW][DATA_GATEWAYS][call.data[ATTR_GW_ID]]
331 await gw_hub.gateway.set_ch_enable_bit(1
if call.data[ATTR_CH_OVRD]
else 0)
333 hass.services.async_register(
337 service_set_central_heating_ovrd_schema,
340 async
def set_control_setpoint(call: ServiceCall) ->
None:
341 """Set the control setpoint on the OpenTherm Gateway."""
342 gw_hub = hass.data[DATA_OPENTHERM_GW][DATA_GATEWAYS][call.data[ATTR_GW_ID]]
343 await gw_hub.gateway.set_control_setpoint(call.data[ATTR_TEMPERATURE])
345 hass.services.async_register(
347 SERVICE_SET_CONTROL_SETPOINT,
348 set_control_setpoint,
349 service_set_control_setpoint_schema,
352 async
def set_dhw_ovrd(call: ServiceCall) ->
None:
353 """Set the domestic hot water override on the OpenTherm Gateway."""
354 gw_hub = hass.data[DATA_OPENTHERM_GW][DATA_GATEWAYS][call.data[ATTR_GW_ID]]
355 await gw_hub.gateway.set_hot_water_ovrd(call.data[ATTR_DHW_OVRD])
357 hass.services.async_register(
359 SERVICE_SET_HOT_WATER_OVRD,
361 service_set_hot_water_ovrd_schema,
364 async
def set_dhw_setpoint(call: ServiceCall) ->
None:
365 """Set the domestic hot water setpoint on the OpenTherm Gateway."""
366 gw_hub = hass.data[DATA_OPENTHERM_GW][DATA_GATEWAYS][call.data[ATTR_GW_ID]]
367 await gw_hub.gateway.set_dhw_setpoint(call.data[ATTR_TEMPERATURE])
369 hass.services.async_register(
371 SERVICE_SET_HOT_WATER_SETPOINT,
373 service_set_hot_water_setpoint_schema,
376 async
def set_device_clock(call: ServiceCall) ->
None:
377 """Set the clock on the OpenTherm Gateway."""
378 gw_hub = hass.data[DATA_OPENTHERM_GW][DATA_GATEWAYS][call.data[ATTR_GW_ID]]
379 attr_date = call.data[ATTR_DATE]
380 attr_time = call.data[ATTR_TIME]
381 await gw_hub.gateway.set_clock(datetime.combine(attr_date, attr_time))
383 hass.services.async_register(
384 DOMAIN, SERVICE_SET_CLOCK, set_device_clock, service_set_clock_schema
388 """Set the OpenTherm Gateway GPIO modes."""
389 gw_hub = hass.data[DATA_OPENTHERM_GW][DATA_GATEWAYS][call.data[ATTR_GW_ID]]
390 gpio_id = call.data[ATTR_ID]
391 gpio_mode = call.data[ATTR_MODE]
392 await gw_hub.gateway.set_gpio_mode(gpio_id, gpio_mode)
394 hass.services.async_register(
395 DOMAIN, SERVICE_SET_GPIO_MODE, set_gpio_mode, service_set_gpio_mode_schema
399 """Set the OpenTherm Gateway LED modes."""
400 gw_hub = hass.data[DATA_OPENTHERM_GW][DATA_GATEWAYS][call.data[ATTR_GW_ID]]
401 led_id = call.data[ATTR_ID]
402 led_mode = call.data[ATTR_MODE]
403 await gw_hub.gateway.set_led_mode(led_id, led_mode)
405 hass.services.async_register(
406 DOMAIN, SERVICE_SET_LED_MODE, set_led_mode, service_set_led_mode_schema
409 async
def set_max_mod(call: ServiceCall) ->
None:
410 """Set the max modulation level."""
411 gw_hub = hass.data[DATA_OPENTHERM_GW][DATA_GATEWAYS][call.data[ATTR_GW_ID]]
412 level = call.data[ATTR_LEVEL]
416 await gw_hub.gateway.set_max_relative_mod(level)
418 hass.services.async_register(
419 DOMAIN, SERVICE_SET_MAX_MOD, set_max_mod, service_set_max_mod_schema
422 async
def set_outside_temp(call: ServiceCall) ->
None:
423 """Provide the outside temperature to the OpenTherm Gateway."""
424 gw_hub = hass.data[DATA_OPENTHERM_GW][DATA_GATEWAYS][call.data[ATTR_GW_ID]]
425 await gw_hub.gateway.set_outside_temp(call.data[ATTR_TEMPERATURE])
427 hass.services.async_register(
428 DOMAIN, SERVICE_SET_OAT, set_outside_temp, service_set_oat_schema
431 async
def set_setback_temp(call: ServiceCall) ->
None:
432 """Set the OpenTherm Gateway SetBack temperature."""
433 gw_hub = hass.data[DATA_OPENTHERM_GW][DATA_GATEWAYS][call.data[ATTR_GW_ID]]
434 await gw_hub.gateway.set_setback_temp(call.data[ATTR_TEMPERATURE])
436 hass.services.async_register(
437 DOMAIN, SERVICE_SET_SB_TEMP, set_setback_temp, service_set_sb_temp_schema
440 async
def send_transparent_cmd(call: ServiceCall) ->
None:
441 """Send a transparent OpenTherm Gateway command."""
442 gw_hub = hass.data[DATA_OPENTHERM_GW][DATA_GATEWAYS][call.data[ATTR_GW_ID]]
443 transp_cmd = call.data[ATTR_TRANSP_CMD]
444 transp_arg = call.data[ATTR_TRANSP_ARG]
445 await gw_hub.gateway.send_transparent_command(transp_cmd, transp_arg)
447 hass.services.async_register(
449 SERVICE_SEND_TRANSP_CMD,
450 send_transparent_cmd,
451 service_send_transp_cmd_schema,
456 """Cleanup and disconnect from gateway."""
457 unload_ok = await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
458 gateway = hass.data[DATA_OPENTHERM_GW][DATA_GATEWAYS][entry.data[CONF_ID]]
459 await gateway.cleanup()
464 """OpenTherm Gateway hub class."""
466 def __init__(self, hass: HomeAssistant, config_entry: ConfigEntry) ->
None:
467 """Initialize the OpenTherm Gateway."""
470 self.
hub_idhub_id = config_entry.data[CONF_ID]
471 self.
namename = config_entry.data[CONF_NAME]
474 self.
update_signalupdate_signal = f
"{DATA_OPENTHERM_GW}_{self.hub_id}_update"
480 """Reset overrides on the gateway."""
481 await self.
gatewaygateway.set_control_setpoint(0)
482 await self.
gatewaygateway.set_max_relative_mod(
"-")
483 await self.
gatewaygateway.disconnect()
486 """Connect to serial device and subscribe report handler."""
490 raise ConnectionError
491 version_string = status[OpenThermDataSource.GATEWAY].
get(gw_vars.OTGW_ABOUT)
492 self.
gw_versiongw_version = version_string[18:]
if version_string
else None
496 dev_reg = dr.async_get(self.
hasshass)
497 gw_dev = dev_reg.async_get_or_create(
500 (DOMAIN, f
"{self.hub_id}-{OpenThermDeviceIdentifier.GATEWAY}")
502 manufacturer=
"Schelte Bron",
503 model=
"OpenTherm Gateway",
504 translation_key=
"gateway_device",
507 if gw_dev.sw_version != self.
gw_versiongw_version:
508 dev_reg.async_update_device(gw_dev.id, sw_version=self.
gw_versiongw_version)
510 boiler_device = dev_reg.async_get_or_create(
512 identifiers={(DOMAIN, f
"{self.hub_id}-{OpenThermDeviceIdentifier.BOILER}")},
513 translation_key=
"boiler_device",
515 thermostat_device = dev_reg.async_get_or_create(
518 (DOMAIN, f
"{self.hub_id}-{OpenThermDeviceIdentifier.THERMOSTAT}")
520 translation_key=
"thermostat_device",
523 self.
hasshass.bus.async_listen(EVENT_HOMEASSISTANT_STOP, self.
cleanupcleanup)
525 async
def handle_report(status):
526 """Handle reports from the OpenTherm Gateway."""
527 _LOGGER.debug(
"Received report: %s", status)
530 dev_reg.async_update_device(
532 manufacturer=status[OpenThermDataSource.BOILER].
get(
533 gw_vars.DATA_SLAVE_MEMBERID
535 model_id=status[OpenThermDataSource.BOILER].
get(
536 gw_vars.DATA_SLAVE_PRODUCT_TYPE
538 hw_version=status[OpenThermDataSource.BOILER].
get(
539 gw_vars.DATA_SLAVE_PRODUCT_VERSION
541 sw_version=status[OpenThermDataSource.BOILER].
get(
542 gw_vars.DATA_SLAVE_OT_VERSION
546 dev_reg.async_update_device(
547 thermostat_device.id,
548 manufacturer=status[OpenThermDataSource.THERMOSTAT].
get(
549 gw_vars.DATA_MASTER_MEMBERID
551 model_id=status[OpenThermDataSource.THERMOSTAT].
get(
552 gw_vars.DATA_MASTER_PRODUCT_TYPE
554 hw_version=status[OpenThermDataSource.THERMOSTAT].
get(
555 gw_vars.DATA_MASTER_PRODUCT_VERSION
557 sw_version=status[OpenThermDataSource.THERMOSTAT].
get(
558 gw_vars.DATA_MASTER_OT_VERSION
566 """Report whether or not we are connected to the gateway."""
567 return self.
gatewaygateway.connection.connected
None cleanup(self, event=None)
None connect_and_subscribe(self)
None __init__(self, HomeAssistant hass, ConfigEntry config_entry)
web.Response get(self, web.Request request, str config_key)
Callable[[], None] subscribe(HomeAssistant hass, str topic, MessageCallbackType msg_callback, int qos=DEFAULT_QOS, str encoding="utf-8")
OpenThermSelectLEDMode|None set_led_mode(str led_id, OpenThermGatewayHub gw_hub, str mode)
OpenThermSelectGPIOMode|None set_gpio_mode(str gpio_id, OpenThermGatewayHub gw_hub, str mode)
None register_services(HomeAssistant hass)
bool async_unload_entry(HomeAssistant hass, ConfigEntry entry)
bool async_setup_entry(HomeAssistant hass, ConfigEntry config_entry)
bool async_setup(HomeAssistant hass, ConfigType config)
None options_updated(HomeAssistant hass, ConfigEntry entry)
bool time(HomeAssistant hass, dt_time|str|None before=None, dt_time|str|None after=None, str|Container[str]|None weekday=None)
None async_dispatcher_send(HomeAssistant hass, str signal, *Any args)