diff --git a/custom_components/askoheat/__init__.py b/custom_components/askoheat/__init__.py index 775f1f6..427b57d 100644 --- a/custom_components/askoheat/__init__.py +++ b/custom_components/askoheat/__init__.py @@ -12,7 +12,16 @@ from homeassistant.const import CONF_HOST, CONF_PORT, Platform from homeassistant.loader import async_get_loaded_integration +from custom_components.askoheat.const import DeviceKey + from .api import AskoHeatModbusApiClient +from .const import ( + CONF_ANALOG_INPUT_UNIT, + CONF_DEVICE_UNITS, + CONF_HEATPUMP_UNIT, + CONF_LEGIONELLA_PROTECTION_UNIT, + CONF_MODBUS_MASTER_UNIT, +) from .coordinator import ( AskoheatConfigDataUpdateCoordinator, AskoheatEMADataUpdateCoordinator, @@ -50,6 +59,20 @@ async def async_setup_entry( ) config_coordinator = AskoheatConfigDataUpdateCoordinator(hass=hass) data_coordinator = AskoheatOperationDataUpdateCoordinator(hass=hass) + + # default devices + supported_devices = [DeviceKey.WATER_HEATER_CONTROL_UNIT, DeviceKey.ENERGY_MANAGER] + # add devices based on configuration + additional_devices = entry.data[CONF_DEVICE_UNITS] + if additional_devices[CONF_LEGIONELLA_PROTECTION_UNIT]: + supported_devices.append(DeviceKey.LEGIO_PROTECTION_CONTROL_UNIT) + if additional_devices[CONF_ANALOG_INPUT_UNIT]: + supported_devices.append(DeviceKey.ANALOG_INPUT_CONTROL_UNIT) + if additional_devices[CONF_MODBUS_MASTER_UNIT]: + supported_devices.append(DeviceKey.MODBUS_MASTER) + if additional_devices[CONF_HEATPUMP_UNIT]: + supported_devices.append(DeviceKey.HEATPUMP_CONTROL_UNIT) + entry.runtime_data = AskoheatData( client=AskoHeatModbusApiClient( host=entry.data[CONF_HOST], @@ -60,6 +83,7 @@ async def async_setup_entry( config_coordinator=config_coordinator, par_coordinator=par_coordinator, data_coordinator=data_coordinator, + supported_devices=supported_devices, ) await entry.runtime_data.client.connect() diff --git a/custom_components/askoheat/api_conf_desc.py b/custom_components/askoheat/api_conf_desc.py index 556ce80..0bc8e21 100644 --- a/custom_components/askoheat/api_conf_desc.py +++ b/custom_components/askoheat/api_conf_desc.py @@ -172,7 +172,7 @@ ), AskoheatNumberEntityDescription( key=NumberAttrKey.CON_RTU_SLAVE_ID, - device_key=DeviceKey.ENERGY_MANAGER, + device_key=DeviceKey.MODBUS_MASTER, native_min_value=0, native_max_value=240, entity_category=EntityCategory.CONFIG, diff --git a/custom_components/askoheat/binary_sensor.py b/custom_components/askoheat/binary_sensor.py index d141500..076047f 100644 --- a/custom_components/askoheat/binary_sensor.py +++ b/custom_components/askoheat/binary_sensor.py @@ -56,6 +56,8 @@ async def async_setup_entry( for entity_description in DATA_REGISTER_BLOCK_DESCRIPTOR.binary_sensors }, }.items() + if entity_description.device_key is None + or entity_description.device_key in entry.runtime_data.supported_devices ) diff --git a/custom_components/askoheat/config_flow.py b/custom_components/askoheat/config_flow.py index 33f558b..5d50eb3 100644 --- a/custom_components/askoheat/config_flow.py +++ b/custom_components/askoheat/config_flow.py @@ -2,6 +2,10 @@ from __future__ import annotations +from types import MappingProxyType +from typing import TYPE_CHECKING, Any + +import homeassistant.helpers.config_validation as cv import voluptuous as vol from homeassistant import config_entries, data_entry_flow from homeassistant.const import CONF_HOST, CONF_PORT @@ -17,6 +21,11 @@ AskoheatModbusApiClientError, ) from .const import ( + CONF_ANALOG_INPUT_UNIT, + CONF_DEVICE_UNITS, + CONF_HEATPUMP_UNIT, + CONF_LEGIONELLA_PROTECTION_UNIT, + CONF_MODBUS_MASTER_UNIT, DEFAULT_HOST, DEFAULT_PORT, DOMAIN, @@ -24,6 +33,9 @@ SensorAttrKey, ) +if TYPE_CHECKING: + from homeassistant.components.dhcp import DhcpServiceInfo + PORT_SELECTOR = vol.All( selector.NumberSelector( selector.NumberSelectorConfig( @@ -33,12 +45,62 @@ vol.Coerce(int), ) -STEP_USER_DATA_SCHEMA = vol.Schema( - { - vol.Required(CONF_HOST, default=DEFAULT_HOST): str, - vol.Required(CONF_PORT, default=DEFAULT_PORT): PORT_SELECTOR, - } -) + +def _step_user_data_schema( + data: MappingProxyType[str, Any] | None = None, +) -> vol.Schema: + return vol.Schema( + { + vol.Required( + CONF_HOST, default=data[CONF_HOST] if data else DEFAULT_HOST + ): cv.string, + vol.Required( + CONF_PORT, default=data[CONF_PORT] if data else DEFAULT_PORT + ): PORT_SELECTOR, + CONF_DEVICE_UNITS: data_entry_flow.section( + vol.Schema( + { + vol.Required( + CONF_LEGIONELLA_PROTECTION_UNIT, + default=data[CONF_DEVICE_UNITS][ + CONF_LEGIONELLA_PROTECTION_UNIT + ] + if data + else True, + ): cv.boolean, + vol.Required( + CONF_HEATPUMP_UNIT, + default=data[CONF_DEVICE_UNITS][ + CONF_LEGIONELLA_PROTECTION_UNIT + ] + if data + else False, + ): cv.boolean, + vol.Required( + CONF_ANALOG_INPUT_UNIT, + default=data[CONF_DEVICE_UNITS][ + CONF_LEGIONELLA_PROTECTION_UNIT + ] + if data + else False, + ): cv.boolean, + vol.Required( + CONF_MODBUS_MASTER_UNIT, + default=data[CONF_DEVICE_UNITS][ + CONF_LEGIONELLA_PROTECTION_UNIT + ] + if data + else False, + ): cv.boolean, + } + ), + {"collapsed": False}, + ), + } + ) + + +STEP_USER_DATA_SCHEMA = _step_user_data_schema() class AskoheatFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): @@ -101,3 +163,23 @@ async def async_step_user( data_schema=STEP_USER_DATA_SCHEMA, errors=_errors, ) + + async def async_step_dhcp( + self, discovery_info: DhcpServiceInfo + ) -> data_entry_flow.FlowResult: + """Prepare configuration for a DHCP discovered Askoheat device.""" + LOGGER.info( + "Found device with hostname '%s' IP '%s'", + discovery_info.hostname, + discovery_info.ip, + ) + # Validate dhcp result with socket broadcast: + config = dict[str, Any]() + config[CONF_HOST] = discovery_info.ip + config[CONF_PORT] = DEFAULT_PORT + + return self.async_show_form( + step_id="user", + data_schema=_step_user_data_schema(MappingProxyType(config)), + errors={}, + ) diff --git a/custom_components/askoheat/const.py b/custom_components/askoheat/const.py index 55b4a36..676027b 100644 --- a/custom_components/askoheat/const.py +++ b/custom_components/askoheat/const.py @@ -18,7 +18,11 @@ SCAN_INTERVAL_CONFIG = timedelta(hours=1) SCAN_INTERVAL_OP_DATA = timedelta(minutes=1) -CONF_DEVICE_UNIQUE_ID = "device_unique_id" +CONF_DEVICE_UNITS = "devices" +CONF_ANALOG_INPUT_UNIT = "analog_input_unit" +CONF_LEGIONELLA_PROTECTION_UNIT = "legionella_protection_unit" +CONF_MODBUS_MASTER_UNIT = "modbus_master_unit" +CONF_HEATPUMP_UNIT = "heatpump_unit" class NumberAttrKey(StrEnum): diff --git a/custom_components/askoheat/data.py b/custom_components/askoheat/data.py index ef0589f..30edb9e 100644 --- a/custom_components/askoheat/data.py +++ b/custom_components/askoheat/data.py @@ -15,6 +15,7 @@ from custom_components.askoheat.const import ( BinarySensorAttrKey, + DeviceKey, NumberAttrKey, SelectAttrKey, SensorAttrKey, @@ -45,6 +46,7 @@ class AskoheatData: par_coordinator: AskoheatParameterDataUpdateCoordinator data_coordinator: AskoheatOperationDataUpdateCoordinator integration: Integration + supported_devices: list[DeviceKey] @dataclass diff --git a/custom_components/askoheat/manifest.json b/custom_components/askoheat/manifest.json index f038bfb..923d4a6 100644 --- a/custom_components/askoheat/manifest.json +++ b/custom_components/askoheat/manifest.json @@ -5,8 +5,12 @@ "@toggm" ], "config_flow": true, + "dhcp": [ + { "hostname": "askoheat" }, + { "hostname": "askoheat.local" } + ], "documentation": "https://github.com/toggm/askoheat", "iot_class": "cloud_polling", "issue_tracker": "https://github.com/toggm/askoheat/issues", - "version": "0.0.0" + "version": "0.0.1-beta" } \ No newline at end of file diff --git a/custom_components/askoheat/number.py b/custom_components/askoheat/number.py index 943f0f8..9fe04ea 100644 --- a/custom_components/askoheat/number.py +++ b/custom_components/askoheat/number.py @@ -51,6 +51,8 @@ async def async_setup_entry( for entity_description in CONF_REGISTER_BLOCK_DESCRIPTOR.number_inputs }, }.items() + if entity_description.device_key is None + or entity_description.device_key in entry.runtime_data.supported_devices ) diff --git a/custom_components/askoheat/select.py b/custom_components/askoheat/select.py index 41868f9..0accae6 100644 --- a/custom_components/askoheat/select.py +++ b/custom_components/askoheat/select.py @@ -45,6 +45,8 @@ async def async_setup_entry( for entity_description in CONF_REGISTER_BLOCK_DESCRIPTOR.select_inputs }, }.items() + if entity_description.device_key is None + or entity_description.device_key in entry.runtime_data.supported_devices ) diff --git a/custom_components/askoheat/sensor.py b/custom_components/askoheat/sensor.py index 052b74e..beefcc7 100644 --- a/custom_components/askoheat/sensor.py +++ b/custom_components/askoheat/sensor.py @@ -84,6 +84,8 @@ async def async_setup_entry( for entity_description in DATA_REGISTER_BLOCK_DESCRIPTOR.sensors }, }.items() + if entity_description.device_key is None + or entity_description.device_key in entry.runtime_data.supported_devices ) diff --git a/custom_components/askoheat/switch.py b/custom_components/askoheat/switch.py index 7690f9d..711f611 100644 --- a/custom_components/askoheat/switch.py +++ b/custom_components/askoheat/switch.py @@ -49,6 +49,8 @@ async def async_setup_entry( for entity_description in CONF_REGISTER_BLOCK_DESCRIPTOR.switches }, }.items() + if entity_description.device_key is None + or entity_description.device_key in entry.runtime_data.supported_devices ) diff --git a/custom_components/askoheat/text.py b/custom_components/askoheat/text.py index 3deadcf..80033d8 100644 --- a/custom_components/askoheat/text.py +++ b/custom_components/askoheat/text.py @@ -43,6 +43,8 @@ async def async_setup_entry( for entity_description in CONF_REGISTER_BLOCK_DESCRIPTOR.text_inputs }, }.items() + if entity_description.device_key is None + or entity_description.device_key in entry.runtime_data.supported_devices ) diff --git a/custom_components/askoheat/time.py b/custom_components/askoheat/time.py index d49a4f0..b556aa0 100644 --- a/custom_components/askoheat/time.py +++ b/custom_components/askoheat/time.py @@ -43,6 +43,8 @@ async def async_setup_entry( for entity_description in CONF_REGISTER_BLOCK_DESCRIPTOR.time_inputs }, }.items() + if entity_description.device_key is None + or entity_description.device_key in entry.runtime_data.supported_devices ) diff --git a/custom_components/askoheat/translations/en.json b/custom_components/askoheat/translations/en.json index 733466e..e50e85b 100644 --- a/custom_components/askoheat/translations/en.json +++ b/custom_components/askoheat/translations/en.json @@ -6,6 +6,17 @@ "data": { "host": "host", "port": "port" + }, + "sections": { + "devices": { + "name": "Enable additional device units", + "data": { + "legionella_protection_unit": "Legionella protection", + "heatpump_unit": "Heatpump control", + "analog_input_unit": "Analog input control", + "modbus_master_unit": "Modbus master" + } + } } } },