├── requirements.txt ├── custom_components ├── __init__.py └── waterkotte_heatpump │ ├── icons.json │ ├── manifest.json │ ├── translations │ ├── fr.json │ ├── en.json │ └── de.json │ ├── pywaterkotte_ha │ ├── error.py │ └── __init__.py │ ├── select.py │ ├── number.py │ ├── switch.py │ ├── sensor.py │ ├── binary_sensor.py │ ├── services.yaml │ ├── strings.json │ ├── config_flow.py │ ├── __init__.py │ └── service.py ├── logo.png ├── requirements_dev.txt ├── sample-view.png ├── login_easycon.png ├── login_ecotouch.png ├── sample-view-s.png ├── .github ├── FUNDING.yml ├── workflows │ ├── hassfest.yaml │ └── hacs.yaml └── ISSUE_TEMPLATE │ ├── 2-feature-request.yaml │ └── 1-report-an-issue.yaml ├── .gitignore ├── hacs.json ├── LICENSE ├── generator └── scheduler_objs.py ├── sample-view.yaml └── README.md /requirements.txt: -------------------------------------------------------------------------------- 1 | homeassistant>=2024.8.2 -------------------------------------------------------------------------------- /custom_components/__init__.py: -------------------------------------------------------------------------------- 1 | """Dummy init so that pytest works.""" 2 | -------------------------------------------------------------------------------- /logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marq24/ha-waterkotte/HEAD/logo.png -------------------------------------------------------------------------------- /requirements_dev.txt: -------------------------------------------------------------------------------- 1 | pytest-homeassistant-custom-component>=0.13.154 2 | -------------------------------------------------------------------------------- /sample-view.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marq24/ha-waterkotte/HEAD/sample-view.png -------------------------------------------------------------------------------- /login_easycon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marq24/ha-waterkotte/HEAD/login_easycon.png -------------------------------------------------------------------------------- /login_ecotouch.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marq24/ha-waterkotte/HEAD/login_ecotouch.png -------------------------------------------------------------------------------- /sample-view-s.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/marq24/ha-waterkotte/HEAD/sample-view-s.png -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: marq24 2 | #buy_me_a_coffee: marquardt24 3 | #custom: https://paypal.me/marq24 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | pythonenv* 3 | .python-version 4 | .coverage 5 | venv 6 | .venv 7 | core.* 8 | *.iml 9 | /generator/gen_*.txt 10 | -------------------------------------------------------------------------------- /hacs.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Waterkotte Heatpump [+2020]", 3 | "homeassistant": "2023.8.0", 4 | "hacs": "1.18.0", 5 | "render_readme": true 6 | } 7 | -------------------------------------------------------------------------------- /.github/workflows/hassfest.yaml: -------------------------------------------------------------------------------- 1 | name: Validate with hassfest 2 | 3 | on: 4 | push: 5 | pull_request: 6 | schedule: 7 | - cron: '0 0 * * *' 8 | 9 | jobs: 10 | validate: 11 | runs-on: "ubuntu-latest" 12 | steps: 13 | - uses: "actions/checkout@v2" 14 | - uses: "home-assistant/actions/hassfest@master" 15 | -------------------------------------------------------------------------------- /.github/workflows/hacs.yaml: -------------------------------------------------------------------------------- 1 | name: HACS Action 2 | 3 | on: 4 | push: 5 | pull_request: 6 | schedule: 7 | - cron: "0 0 * * *" 8 | 9 | jobs: 10 | hacs: 11 | name: HACS Action 12 | runs-on: "ubuntu-latest" 13 | steps: 14 | - uses: "actions/checkout@v2" 15 | - name: HACS Action 16 | uses: "hacs/action@main" 17 | with: 18 | category: "integration" 19 | -------------------------------------------------------------------------------- /custom_components/waterkotte_heatpump/icons.json: -------------------------------------------------------------------------------- 1 | { 2 | "services": { 3 | "sync_time": "mdi:clock-outline", 4 | "set_holiday": "mdi:calendar", 5 | "set_disinfection_start_time": "mdi:calendar", 6 | "set_schedule_data": "mdi:calendar-clock", 7 | "get_energy_balance": "mdi:home-lightning-bolt-outline", 8 | "get_energy_balance_monthly": "mdi:home-lightning-bolt-outline" 9 | } 10 | } -------------------------------------------------------------------------------- /custom_components/waterkotte_heatpump/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "domain": "waterkotte_heatpump", 3 | "name": "Waterkotte Heatpump [+2020]", 4 | "codeowners": [ 5 | "@marq24", 6 | "@pattisonmichael" 7 | ], 8 | "config_flow": true, 9 | "dependencies": [], 10 | "documentation": "https://github.com/marq24/ha-waterkotte", 11 | "iot_class": "local_polling", 12 | "issue_tracker": "https://github.com/marq24/ha-waterkotte/issues", 13 | "requirements": [], 14 | "version": "2025.9.0" 15 | } 16 | -------------------------------------------------------------------------------- /custom_components/waterkotte_heatpump/translations/fr.json: -------------------------------------------------------------------------------- 1 | { 2 | "entity": { 3 | "select": { 4 | "tkey_temperature_heating_mode": { 5 | "name": "Régulation chauffage", 6 | "state": { 7 | "hm0": "Météo-compensé", 8 | "hm1": "Consigne", 9 | "hm2": "Consigne BMS", 10 | "hm3": "Consigne EXT", 11 | "hm4": "Consigne 0-10V", 12 | "hm5": "Consigne circuit mélangeur" 13 | } 14 | } 15 | }, 16 | "number": { 17 | "source_pump_capture_temperature_a479":{"name": "Pompe captage - ΔT captage"} 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/2-feature-request.yaml: -------------------------------------------------------------------------------- 1 | name: Feature Request 2 | description: Suggest an idea for this home assistant integration 3 | labels: enhancement 4 | body: 5 | - type: textarea 6 | id: content 7 | attributes: 8 | label: Description 9 | placeholder: "Let me know what do you miss..." 10 | - type: textarea 11 | id: logs 12 | attributes: 13 | label: List of tags 14 | placeholder: "Please use the inspect feature of your browser (while using the default waterkotte web client) in order to extract the corresponding TAGs that should be used to previde the requested data - TIA" 15 | render: shell -------------------------------------------------------------------------------- /custom_components/waterkotte_heatpump/pywaterkotte_ha/error.py: -------------------------------------------------------------------------------- 1 | class InvalidValueException(Exception): 2 | """A InvalidValueException.""" 3 | 4 | # pass 5 | 6 | 7 | class InvalidResponseException(Exception): 8 | """A InvalidResponseException.""" 9 | 10 | # pass 11 | 12 | 13 | class StatusException(Exception): 14 | """A Status Exception.""" 15 | 16 | # pass 17 | 18 | 19 | class TooManyUsersException(StatusException): 20 | """A TooManyUsers Exception.""" 21 | # pass 22 | 23 | class InvalidPasswordException(StatusException): 24 | """A TooManyUsers Exception.""" 25 | # pass 26 | 27 | class Http404Exception(Exception): 28 | """A HTTP404 Exception.""" 29 | # pass 30 | 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 pattisonmichael 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/1-report-an-issue.yaml: -------------------------------------------------------------------------------- 1 | name: Problem Report 2 | description: please report any technical issue with this home assistant integration - please note this is not a official Waterkotte repository or service 3 | labels: bug 4 | body: 5 | - type: checkboxes 6 | id: checklist 7 | attributes: 8 | label: Checklist 9 | description: Please go though this short checklist - TIA 10 | options: 11 | - label: I have installed the **latest** release (or BETA) version of the integration and home assistant. 12 | required: true 13 | - label: I have prepared DEBUG log output (for technical issues) | In most of the cases of a technical error/issue I would have the need to ask for DEBUG log output of the integration. There is a short [tutorial/guide 'How to provide DEBUG log' here](https://github.com/marq24/ha-senec-v3/blob/main/docs/HA_DEBUG.md) 14 | required: true 15 | - label: I confirm it's really an issue | In the case that you want to understand the functionality of a certain feature/sensor Please be so kind and make use if the discussion feature of this repo (and do not create an issue) - TIA 16 | - type: textarea 17 | id: content 18 | attributes: 19 | label: Add a description 20 | placeholder: "Please provide details about your issue - in the best case a short step by step instruction how to reproduce the issue - TIA." 21 | - type: textarea 22 | id: logs 23 | attributes: 24 | label: Add your DEBUG log output 25 | placeholder: "Please copy and paste any relevant log output. This will be automatically formatted into code, so no need for backticks." 26 | render: shell -------------------------------------------------------------------------------- /custom_components/waterkotte_heatpump/select.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from homeassistant.components.select import SelectEntity 4 | from homeassistant.config_entries import ConfigEntry 5 | from homeassistant.core import HomeAssistant 6 | from homeassistant.helpers.entity_platform import AddEntitiesCallback 7 | 8 | from . import WKHPDataUpdateCoordinator, WKHPBaseEntity 9 | from .const import DOMAIN, SELECT_SENSORS, ExtSelectEntityDescription 10 | from .const_gen import SELECT_SENSORS_GENERATED 11 | 12 | _LOGGER = logging.getLogger(__name__) 13 | 14 | 15 | async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, add_entity_cb: AddEntitiesCallback): 16 | _LOGGER.debug("SELECT async_setup_entry") 17 | coordinator = hass.data[DOMAIN][config_entry.entry_id] 18 | entities = [] 19 | for description in SELECT_SENSORS: 20 | entity = WKHPSelect(coordinator, description) 21 | entities.append(entity) 22 | if coordinator.add_schedule_entities: 23 | for description in SELECT_SENSORS_GENERATED: 24 | entity = WKHPSelect(coordinator, description) 25 | entities.append(entity) 26 | add_entity_cb(entities) 27 | 28 | 29 | class WKHPSelect(WKHPBaseEntity, SelectEntity): 30 | def __init__(self, coordinator: WKHPDataUpdateCoordinator, description: ExtSelectEntityDescription): 31 | super().__init__(coordinator=coordinator, description=description) 32 | 33 | @property 34 | def current_option(self) -> str | None: 35 | try: 36 | value = self.coordinator.data[self.wkhp_tag]["value"] 37 | if value is None or value == "": 38 | value = 'unknown' 39 | elif isinstance(value, bool): 40 | # for "switches" that we want to show as selects, we need to convert 41 | # the bool True/False to 1 and 0 42 | if value: 43 | value = "1" 44 | else: 45 | value = "0" 46 | except KeyError: 47 | value = "unknown" 48 | except TypeError: 49 | return None 50 | return str(value) 51 | 52 | async def async_select_option(self, option: str) -> None: 53 | try: 54 | await self.coordinator.async_write_tag(self.wkhp_tag, option, self) 55 | except ValueError: 56 | return "unavailable" 57 | -------------------------------------------------------------------------------- /custom_components/waterkotte_heatpump/number.py: -------------------------------------------------------------------------------- 1 | import logging 2 | 3 | from homeassistant.components.number import NumberEntity 4 | from homeassistant.config_entries import ConfigEntry 5 | from homeassistant.helpers.entity_platform import AddEntitiesCallback 6 | from homeassistant.core import HomeAssistant 7 | 8 | from . import WKHPDataUpdateCoordinator, WKHPBaseEntity 9 | from .const import DOMAIN, NUMBER_SENSORS, ExtNumberEntityDescription 10 | from .const_gen import NUMBER_SENSORS_GENERATED 11 | 12 | _LOGGER = logging.getLogger(__name__) 13 | 14 | TEMP_ADJUST_LOOKUP = [-2, -1.5, -1, -0.5, 0, 0.5, 1, 1.5, 2] 15 | 16 | 17 | async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, add_entity_cb: AddEntitiesCallback): 18 | _LOGGER.debug("NUMBER async_setup_entry") 19 | coordinator = hass.data[DOMAIN][config_entry.entry_id] 20 | entities = [] 21 | for description in NUMBER_SENSORS: 22 | entity = WKHPNumber(coordinator, description) 23 | entities.append(entity) 24 | if coordinator.add_schedule_entities: 25 | for description in NUMBER_SENSORS_GENERATED: 26 | entity = WKHPNumber(coordinator, description) 27 | entities.append(entity) 28 | add_entity_cb(entities) 29 | 30 | 31 | class WKHPNumber(WKHPBaseEntity, NumberEntity): 32 | def __init__(self, coordinator: WKHPDataUpdateCoordinator, description: ExtNumberEntityDescription): 33 | super().__init__(coordinator=coordinator, description=description) 34 | 35 | @property 36 | def native_value(self) -> float | None: 37 | try: 38 | value = self.coordinator.data[self.wkhp_tag]["value"] 39 | if value is None or value == "": 40 | return "unknown" 41 | if str(self.wkhp_tag.name).upper().endswith("_ADJUST"): 42 | value = TEMP_ADJUST_LOOKUP[value] 43 | except KeyError: 44 | return "unknown" 45 | except TypeError: 46 | return None 47 | return float(value) 48 | 49 | async def async_set_native_value(self, value: float) -> None: 50 | try: 51 | if str(self.wkhp_tag.name).upper().endswith("_ADJUST"): 52 | value = TEMP_ADJUST_LOOKUP.index(value) 53 | if self.wkhp_tag[0][0][0] == 'I': 54 | value = int(value) 55 | await self.coordinator.async_write_tag(self.wkhp_tag, value, self) 56 | except ValueError: 57 | return "unavailable" 58 | -------------------------------------------------------------------------------- /custom_components/waterkotte_heatpump/switch.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from typing import Literal 3 | 4 | from homeassistant.config_entries import ConfigEntry 5 | from homeassistant.const import STATE_ON, STATE_OFF 6 | from homeassistant.components.switch import SwitchEntity 7 | from homeassistant.helpers.entity_platform import AddEntitiesCallback 8 | from homeassistant.core import HomeAssistant 9 | 10 | from . import WKHPDataUpdateCoordinator, WKHPBaseEntity 11 | from .const import DOMAIN, SWITCH_SENSORS, ExtSwitchEntityDescription 12 | from .const_gen import SWITCH_SENSORS_GENERATED 13 | 14 | _LOGGER = logging.getLogger(__name__) 15 | 16 | 17 | async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, add_entity_cb: AddEntitiesCallback): 18 | _LOGGER.debug("SWITCH async_setup_entry") 19 | coordinator = hass.data[DOMAIN][config_entry.entry_id] 20 | entities = [] 21 | for description in SWITCH_SENSORS: 22 | entity = WKHPSwitch(coordinator, description) 23 | entities.append(entity) 24 | if coordinator.add_schedule_entities: 25 | for description in SWITCH_SENSORS_GENERATED: 26 | entity = WKHPSwitch(coordinator, description) 27 | entities.append(entity) 28 | add_entity_cb(entities) 29 | 30 | 31 | class WKHPSwitch(WKHPBaseEntity, SwitchEntity): 32 | def __init__(self, coordinator: WKHPDataUpdateCoordinator, description: ExtSwitchEntityDescription): 33 | super().__init__(coordinator=coordinator, description=description) 34 | self._attr_icon_off = self.entity_description.icon_off 35 | 36 | async def async_turn_on(self, **kwargs): 37 | """Turn on the switch.""" 38 | try: 39 | await self.coordinator.async_write_tag(self.wkhp_tag, True, self) 40 | return self.coordinator.data[self.wkhp_tag]["value"] 41 | except ValueError: 42 | return "unavailable" 43 | 44 | async def async_turn_off(self, **kwargs): 45 | """Turn off the switch.""" 46 | try: 47 | await self.coordinator.async_write_tag(self.wkhp_tag, False, self) 48 | return self.coordinator.data[self.wkhp_tag]["value"] 49 | except ValueError: 50 | return "unavailable" 51 | 52 | @property 53 | def is_on(self) -> bool | None: 54 | try: 55 | value = None 56 | if self.wkhp_tag in self.coordinator.data: 57 | value_and_state = self.coordinator.data[self.wkhp_tag] 58 | # _LOGGER.error(f"{self.entity_description.key} -> {value_and_state}") 59 | if "value" in value_and_state: 60 | value = value_and_state["value"] 61 | else: 62 | _LOGGER.debug( 63 | f"is_on: for {self.entity_description.key} could not read value from data: {value_and_state}") 64 | else: 65 | if len(self.coordinator.data) > 0: 66 | _LOGGER.debug( 67 | f"is_on: for {self.entity_description.key} not found in data: {len(self.coordinator.data)}") 68 | if value is None or value == "": 69 | value = None 70 | except KeyError: 71 | _LOGGER.warning(f"is_on caused KeyError for: {self.entity_description.key}") 72 | value = None 73 | except TypeError: 74 | return None 75 | return value 76 | 77 | @property 78 | def state(self) -> Literal["on", "off"] | None: 79 | """Return the state.""" 80 | if (is_on := self.is_on) is None: 81 | return None 82 | return STATE_ON if is_on else STATE_OFF 83 | 84 | @property 85 | def icon(self): 86 | """Return the icon of the sensor.""" 87 | if self._attr_icon_off is not None and self.state == STATE_OFF: 88 | return self._attr_icon_off 89 | else: 90 | return super().icon 91 | -------------------------------------------------------------------------------- /custom_components/waterkotte_heatpump/sensor.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from datetime import datetime, time 3 | 4 | from homeassistant.components.sensor import SensorEntity, SensorDeviceClass 5 | from homeassistant.config_entries import ConfigEntry 6 | from homeassistant.const import EntityCategory 7 | from homeassistant.core import HomeAssistant 8 | from homeassistant.helpers.entity_platform import AddEntitiesCallback 9 | from homeassistant.helpers.restore_state import RestoreEntity 10 | from . import WKHPDataUpdateCoordinator, WKHPBaseEntity 11 | from .const import DOMAIN, SENSOR_SENSORS, ExtSensorEntityDescription 12 | from .const_gen import SENSOR_SENSORS_GENERATED 13 | 14 | _LOGGER = logging.getLogger(__name__) 15 | 16 | 17 | async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, add_entity_cb: AddEntitiesCallback): 18 | _LOGGER.debug("SENSOR async_setup_entry") 19 | coordinator = hass.data[DOMAIN][config_entry.entry_id] 20 | entities = [] 21 | for description in SENSOR_SENSORS: 22 | entity = WKHPSensor(coordinator, description) 23 | entities.append(entity) 24 | if coordinator.add_schedule_entities: 25 | for description in SENSOR_SENSORS_GENERATED: 26 | entity = WKHPSensor(coordinator, description) 27 | entities.append(entity) 28 | add_entity_cb(entities) 29 | 30 | 31 | class WKHPSensor(WKHPBaseEntity, SensorEntity, RestoreEntity): 32 | def __init__(self, coordinator: WKHPDataUpdateCoordinator, description: ExtSensorEntityDescription): 33 | super().__init__(coordinator=coordinator, description=description) 34 | 35 | # if description.device_class is not None and description.device_class.SensorDeviceClass.DATE: 36 | # if description.tag == WKHPTag.SCHEDULE_WATER_DISINFECTION_START_TIME: 37 | # self._attr_native_value = time 38 | # else: 39 | # self._attr_native_value = datetime 40 | 41 | # self._previous_float_value: float | None = None 42 | # self._is_total_increasing: bool = description is not None and isinstance(description, 43 | # ExtSensorEntityDescription) and hasattr( 44 | # description, "controls") and description.controls is not None and "only_increasing" in description.controls 45 | 46 | @property 47 | def _is_bit_field(self) -> bool: 48 | return self.entity_description.key == "ALARM_BITS" or self.entity_description.key == "INTERRUPTION_BITS" 49 | 50 | @property 51 | def state(self): 52 | # for SensorDeviceClass.DATE we will use out OWN 'state' render impl!!! 53 | if self.entity_description.device_class == SensorDeviceClass.DATE: 54 | value = self.native_value 55 | if value is None: 56 | value = "unknown" 57 | return value 58 | else: 59 | return SensorEntity.state.fget(self) 60 | 61 | @property 62 | def native_value(self): 63 | """Return the state of the sensor.""" 64 | try: 65 | value = self.coordinator.data[self.wkhp_tag]["value"] 66 | if value is None or len(str(value)) == 0: 67 | if self._is_bit_field: 68 | value = "none" 69 | else: 70 | value = None 71 | else: 72 | if isinstance(value, datetime): 73 | return value.isoformat(sep=' ', timespec="minutes") 74 | elif isinstance(value, time): 75 | return value.isoformat(timespec="minutes") 76 | elif isinstance(value, bool): 77 | if value is True: 78 | value = "on" 79 | elif value is False: 80 | value = "off" 81 | 82 | except (KeyError, TypeError): 83 | value = None 84 | 85 | # final return statement... 86 | return value 87 | 88 | @property 89 | def entity_category(self): 90 | if self._is_bit_field: 91 | return EntityCategory.DIAGNOSTIC 92 | elif self.entity_description.entity_category is not None: 93 | return self.entity_description.entity_category 94 | return None 95 | -------------------------------------------------------------------------------- /custom_components/waterkotte_heatpump/binary_sensor.py: -------------------------------------------------------------------------------- 1 | import logging 2 | from homeassistant.components.binary_sensor import BinarySensorEntity 3 | from homeassistant.config_entries import ConfigEntry 4 | from homeassistant.helpers.entity_platform import AddEntitiesCallback 5 | from homeassistant.core import HomeAssistant 6 | 7 | from . import WKHPDataUpdateCoordinator, WKHPBaseEntity 8 | from .const import DOMAIN, BINARY_SENSORS, ExtBinarySensorEntityDescription 9 | from .const_gen import BINARY_SENSORS_GENERATED 10 | 11 | _LOGGER = logging.getLogger(__name__) 12 | 13 | 14 | async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry, add_entity_cb: AddEntitiesCallback): 15 | _LOGGER.debug("BINARY_SENSOR async_setup_entry") 16 | coordinator = hass.data[DOMAIN][config_entry.entry_id] 17 | entities = [] 18 | for description in BINARY_SENSORS: 19 | entity = WKHPBinarySensor(coordinator, description) 20 | entities.append(entity) 21 | if coordinator.add_schedule_entities: 22 | for description in BINARY_SENSORS_GENERATED: 23 | entity = WKHPBinarySensor(coordinator, description) 24 | entities.append(entity) 25 | add_entity_cb(entities) 26 | 27 | 28 | class WKHPBinarySensor(WKHPBaseEntity, BinarySensorEntity): 29 | def __init__(self, coordinator: WKHPDataUpdateCoordinator, description: ExtBinarySensorEntityDescription): 30 | super().__init__(coordinator=coordinator, description=description) 31 | 32 | @property 33 | def is_on(self) -> bool | None: 34 | try: 35 | value = None 36 | if self.wkhp_tag in self.coordinator.data: 37 | value_and_state = self.coordinator.data[self.wkhp_tag] 38 | if "value" in value_and_state: 39 | value = value_and_state["value"] 40 | else: 41 | _LOGGER.debug( 42 | f"is_on: for {self.entity_description.key} could not read value from data: {value_and_state}") 43 | else: 44 | if len(self.coordinator.data) > 0: 45 | _LOGGER.debug( 46 | f"is_on: for {self.entity_description.key} not found in data: {len(self.coordinator.data)}") 47 | if value is None or value == "": 48 | value = None 49 | 50 | except KeyError: 51 | _LOGGER.warning(f"is_on caused KeyError for: {self._type}") 52 | value = None 53 | except TypeError: 54 | return None 55 | 56 | if not isinstance(value, bool): 57 | if isinstance(value, str): 58 | # parse anything else then 'on' to False! 59 | if value.lower() == 'on': 60 | value = True 61 | else: 62 | value = False 63 | else: 64 | value = False 65 | 66 | return value 67 | 68 | @property 69 | def icon(self): 70 | ret = super().icon; 71 | 72 | if ret is not None: 73 | return ret 74 | else: 75 | if self.is_on: 76 | match self.entity_description.key: 77 | case "STATE_HEATING_CIRCULATION_PUMP_D425" | \ 78 | "STATE_BUFFERTANK_CIRCULATION_PUMP_D377" | \ 79 | "STATE_POOL_CIRCULATION_PUMP_D549" | \ 80 | "STATE_MIX1_CIRCULATION_PUMP_D248" | \ 81 | "STATE_MIX2_CIRCULATION_PUMP_D291" | \ 82 | "STATE_MIX3_CIRCULATION_PUMP_D334" | \ 83 | "STATUS_HEATING_CIRCULATION_PUMP" | \ 84 | "STATUS_SOLAR_CIRCULATION_PUMP" | \ 85 | "STATUS_BUFFER_TANK_CIRCULATION_PUMP": 86 | return "mdi:pump" 87 | case _: 88 | return None 89 | else: 90 | match self.entity_description.key: 91 | case "STATE_HEATING_CIRCULATION_PUMP_D425" | \ 92 | "STATE_BUFFERTANK_CIRCULATION_PUMP_D377" | \ 93 | "STATE_POOL_CIRCULATION_PUMP_D549" | \ 94 | "STATE_MIX1_CIRCULATION_PUMP_D248" | \ 95 | "STATE_MIX2_CIRCULATION_PUMP_D291" | \ 96 | "STATE_MIX3_CIRCULATION_PUMP_D334" | \ 97 | "STATUS_HEATING_CIRCULATION_PUMP" | \ 98 | "STATUS_SOLAR_CIRCULATION_PUMP" | \ 99 | "STATUS_BUFFER_TANK_CIRCULATION_PUMP": 100 | return "mdi:pump-off" 101 | case _: 102 | return None 103 | -------------------------------------------------------------------------------- /custom_components/waterkotte_heatpump/services.yaml: -------------------------------------------------------------------------------- 1 | set_holiday: 2 | # Service name as shown in UI 3 | name: Set Holiday 4 | # Description of the service 5 | description: Sets start and end times for holiday mode. 6 | # If the service accepts entity IDs, target allows the user to specify entities by entity, device, or area. If `target` is specified, `entity_id` should not be defined in the `fields` map. By default it shows only targets matching entities from the same domain as the service, but if further customization is required, target supports the entity, device, and area selectors (https://www.home-assistant.io/docs/blueprint/selectors/). Entity selector parameters will automatically be applied to device and area, and device selector parameters will automatically be applied to area. 7 | #target: 8 | # Different fields that your service accepts 9 | fields: 10 | # Key of the field 11 | start: 12 | # Field name as shown in UI 13 | name: Start Time & Date 14 | # Description of the field 15 | description: Set the beginning of the holiday 16 | # Whether or not field is required (default = false) 17 | required: true 18 | # Advanced fields are only shown when the advanced mode is enabled for the user (default = false) 19 | # advanced: true 20 | # Example value that can be passed for this field 21 | #example: "low" 22 | # The default field value 23 | #default: "high" 24 | selector: 25 | datetime: 26 | # Selector (https://www.home-assistant.io/docs/blueprint/selectors/) to control the input UI for this field 27 | #selector: 28 | # select: 29 | # options: 30 | # - "off" 31 | # - "low" 32 | # - "medium" 33 | # - "high" 34 | 35 | end: 36 | name: End Time & Date 37 | description: Set the end of the holiday 38 | required: true 39 | selector: 40 | datetime: 41 | 42 | set_disinfection_start_time: 43 | # Service name as shown in UI 44 | name: Set disinfection start time 45 | # Description of the service 46 | description: Set the start time for disinfection 47 | # If the service accepts entity IDs, target allows the user to specify entities by entity, device, or area. If `target` is specified, `entity_id` should not be defined in the `fields` map. By default it shows only targets matching entities from the same domain as the service, but if further customization is required, target supports the entity, device, and area selectors (https://www.home-assistant.io/docs/blueprint/selectors/). Entity selector parameters will automatically be applied to device and area, and device selector parameters will automatically be applied to area. 48 | #target: 49 | # Different fields that your service accepts 50 | fields: 51 | # Key of the field 52 | starthhmm: 53 | # Field name as shown in UI 54 | name: Disinfection Start Time 55 | # Description of the field 56 | description: Set the disinfection start time 57 | # Whether or not field is required (default = false) 58 | required: true 59 | # Advanced fields are only shown when the advanced mode is enabled for the user (default = false) 60 | # advanced: true 61 | # Example value that can be passed for this field 62 | #example: "low" 63 | # The default field value 64 | #default: "high" 65 | selector: 66 | time: 67 | # Selector (https://www.home-assistant.io/docs/blueprint/selectors/) to control the input UI for this field 68 | #selector: 69 | # select: 70 | # options: 71 | # - "off" 72 | # - "low" 73 | # - "medium" 74 | # - "high" 75 | 76 | set_schedule_data: 77 | # Service name as shown in UI 78 | name: Set a Schedule 79 | # Description of the service 80 | description: Setting the Schedule for a Type 81 | fields: 82 | schedule_type: 83 | name: "Type" 84 | description: "Select the Schedule you would like to adjust" 85 | required: true 86 | default: "heating" 87 | selector: 88 | select: 89 | multiple: false 90 | mode: dropdown 91 | translation_key: "set_schedule_data_schedule_type" 92 | options: [ "heating", "water", "cooling", "mix1", "mix2", "mix3", "pool", "buffer_tank_circulation_pump", "solar", "pv" ] 93 | 94 | enable: 95 | name: "Activate Schedule" 96 | description: " " 97 | required: true 98 | default: true 99 | selector: 100 | boolean: 101 | start_time: 102 | name: "Begin at" 103 | description: " " 104 | required: true 105 | default: "00:00:00" 106 | selector: 107 | time: 108 | end_time: 109 | name: "End at" 110 | description: " " 111 | required: true 112 | default: "00:00:00" 113 | selector: 114 | time: 115 | adj1_enable: 116 | name: "Activate adjustment I" 117 | description: " " 118 | required: false 119 | default: false 120 | selector: 121 | boolean: 122 | adj1_value: 123 | name: "Adjustment I value" 124 | description: " " 125 | required: false 126 | default: "0.0" 127 | selector: 128 | number: 129 | min: -10.0 130 | max: +10.0 131 | step: 0.1 132 | unit_of_measurement: "°K" 133 | mode: box 134 | adj1_start_time: 135 | name: "Adjustment I begin at" 136 | description: " " 137 | required: false 138 | default: "00:00:00" 139 | selector: 140 | time: 141 | adj1_end_time: 142 | name: "Adjustment I end at" 143 | description: " " 144 | required: false 145 | default: "00:00:00" 146 | selector: 147 | time: 148 | adj2_enable: 149 | name: "Activate adjustment II" 150 | description: " " 151 | required: false 152 | default: false 153 | selector: 154 | boolean: 155 | adj2_value: 156 | name: "Adjustment II value" 157 | description: " " 158 | required: false 159 | default: "0.0" 160 | selector: 161 | number: 162 | min: -10.0 163 | max: +10.0 164 | step: 0.1 165 | unit_of_measurement: "°K" 166 | mode: box 167 | adj2_start_time: 168 | name: "Adjustment II begin at" 169 | description: " " 170 | required: false 171 | default: "00:00:00" 172 | selector: 173 | time: 174 | adj2_end_time: 175 | name: "Adjustment II end at" 176 | description: " " 177 | required: false 178 | default: "00:00:00" 179 | selector: 180 | time: 181 | schedule_days: 182 | name: "Days" 183 | description: "Select the days you want to apply the setting of the Schedule" 184 | required: true 185 | selector: 186 | select: 187 | multiple: true 188 | mode: list 189 | translation_key: "set_schedule_data_schedule_days" 190 | options: ["1mo", "2tu", "3we", "4th", "5fr", "6sa", "7su"] 191 | 192 | get_energy_balance: 193 | # Service name as shown in UI 194 | name: Get Current Year Energy Balance 195 | # Description of the service 196 | description: Get the energy balance by different usage for the current year 197 | 198 | get_energy_balance_monthly: 199 | # Service name as shown in UI 200 | name: Get rolling 12 Month breakdown 201 | # Description of the service 202 | description: Gets the energy balance breakdown per month in a rolling 12 month window 203 | -------------------------------------------------------------------------------- /generator/scheduler_objs.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | from typing import Final 3 | 4 | SCHEDULE_LIST: Final = [ 5 | "SCHEDULE_HEATING", 6 | "SCHEDULE_COOLING", 7 | "SCHEDULE_WATER", 8 | "SCHEDULE_POOL", 9 | "SCHEDULE_MIX1", 10 | "SCHEDULE_MIX2", 11 | "SCHEDULE_MIX3", 12 | "SCHEDULE_BUFFER_TANK_CIRCULATION_PUMP", 13 | "SCHEDULE_SOLAR", 14 | "SCHEDULE_PV" 15 | ] 16 | SCHEDULE_DAY_LIST: Final = ["1MO", "2TU", "3WE", "4TH", "5FR", "6SA", "7SU"] 17 | SCHEDULE_SENSOR_TYPES_LIST: Final = ["_ENABLE", "_START_TIME", "_END_TIME", 18 | "_ADJUST1_ENABLE", "_ADJUST1_VALUE", "_ADJUST1_START_TIME", "_ADJUST1_END_TIME", 19 | "_ADJUST2_ENABLE", "_ADJUST2_VALUE", "_ADJUST2_START_TIME", "_ADJUST2_END_TIME"] 20 | 21 | 22 | def generateTags(): 23 | with open("gen_TAGS.txt", 'w+') as out: 24 | values = [ 25 | ["SCHEDULE_HEATING", 42, 63, 151, 179, 207, 235], 26 | ["SCHEDULE_COOLING", 86, 112, 276, 304, 332, 360], 27 | ["SCHEDULE_WATER", 125, 141, 393, 421, 449, 447], 28 | ["SCHEDULE_POOL", 168, 176, 528, 556, 584, 612], 29 | ["SCHEDULE_MIX1", 259, 247, 777, 805, 833, 861], 30 | ["SCHEDULE_MIX2", 302, 293, 897, 925, 953, 981], 31 | ["SCHEDULE_MIX3", 345, 339, 1018, 1046, 1074, 1102], 32 | ["SCHEDULE_BUFFER_TANK_CIRCULATION_PUMP", 388, 385, 1139, 1167, 1195, 1223], 33 | ["SCHEDULE_SOLAR", 204, -1, 648, 676, 704, 732], 34 | ["SCHEDULE_PV", 642, -1, 1483, 1511, 1539, 1567] 35 | ] 36 | 37 | for a_value in values: 38 | day_addon = 0 39 | no_adj_values = a_value[2] == -1 40 | 41 | for a_day in SCHEDULE_DAY_LIST: 42 | enable_idx = a_value[1] + day_addon 43 | value_idx = a_value[2] + day_addon 44 | start_hh_idx = a_value[3] + day_addon 45 | start_mm_idx = a_value[4] + day_addon 46 | end_hh_idx = a_value[5] + day_addon 47 | end_mm_idx = a_value[6] + day_addon 48 | 49 | tags_v = [ 50 | [enable_idx], [start_hh_idx, start_mm_idx], [end_hh_idx, end_mm_idx], 51 | [enable_idx + 1], [value_idx], [start_hh_idx + 1, start_mm_idx + 1], [end_hh_idx + 1, end_mm_idx + 1], 52 | [enable_idx + 2], [value_idx + 1], [start_hh_idx + 2, start_mm_idx + 2], 53 | [end_hh_idx + 2, end_mm_idx + 2], 54 | ] 55 | 56 | for idx in range(len(SCHEDULE_SENSOR_TYPES_LIST)): 57 | a_type = SCHEDULE_SENSOR_TYPES_LIST[idx] 58 | a_tag_base = tags_v[idx] 59 | 60 | if no_adj_values and ("_VALUE" in a_type or "_ADJUST" in a_type): 61 | pass 62 | else: 63 | a_tag_list = [] 64 | for a_int in a_tag_base: 65 | if a_type.endswith("_ENABLE"): 66 | a_tag_list.append(f"D{(a_int)}") 67 | elif a_type.endswith("_VALUE"): 68 | a_tag_list.append(f"A{(a_int)}") 69 | else: 70 | a_tag_list.append(f"I{(a_int)}") 71 | 72 | name = f"{a_value[0]}_{a_day}{a_type}" 73 | if len(a_tag_list) == 1: 74 | out.write(f" {name} = DataTag({a_tag_list}, writeable=True)\r") 75 | else: 76 | out.write(f" {name} = DataTag(\r") 77 | out.write(f" {a_tag_list}, writeable=True, decode_f=DataTag._decode_time_hhmm, encode_f=DataTag._encode_time_hhmm)\r") 78 | 79 | day_addon = day_addon + 4 80 | 81 | out.flush() 82 | 83 | def generateEntityDesc(): 84 | files = {"S": "gen_switch.txt", "N": "gen_number.txt", "D": "gen_sensor.txt"} 85 | outfiles = {} 86 | with open(files["S"], 'w+') as outfiles["S"], open(files["N"], 'w+') as outfiles["N"], open(files["D"], 'w+') as outfiles["D"]: 87 | for a_value in SCHEDULE_LIST: 88 | no_adj_values = a_value == "SCHEDULE_SOLAR" or a_value == "SCHEDULE_PV" 89 | for a_day in SCHEDULE_DAY_LIST: 90 | for a_type in SCHEDULE_SENSOR_TYPES_LIST: 91 | if no_adj_values and ("_VALUE" in a_type or "_ADJUST" in a_type): 92 | pass 93 | else: 94 | a_key = f"{a_value}_{a_day}{a_type}" 95 | if a_type.endswith("_ENABLE"): 96 | outfiles["S"].write(' ExtSwitchEntityDescription(\r') 97 | outfiles["S"].write(f' key="{a_key}",\r') 98 | outfiles["S"].write(f' tag=WKHPTag.{a_key},\r') 99 | outfiles["S"].write(' icon="mdi:calendar-today",\r') 100 | outfiles["S"].write(' entity_registry_enabled_default=False,\r') 101 | outfiles["S"].write(' feature=FEATURE_CODE_GEN\r') 102 | outfiles["S"].write(' ),\r') 103 | elif a_type.endswith("_VALUE"): 104 | outfiles["N"].write(' ExtNumberEntityDescription(\r') 105 | outfiles["N"].write(f' key="{a_key}",\r') 106 | outfiles["N"].write(f' tag=WKHPTag.{a_key},\r') 107 | outfiles["N"].write(' device_class=NumberDeviceClass.TEMPERATURE,\r') 108 | outfiles["N"].write(' icon="mdi:thermometer",\r') 109 | outfiles["N"].write(' entity_registry_enabled_default=False,\r') 110 | outfiles["N"].write(' native_min_value=-10,\r') 111 | outfiles["N"].write(' native_max_value=10,\r') 112 | outfiles["N"].write(' native_step=TENTH_STEP,\r') 113 | outfiles["N"].write(' mode=NumberMode.BOX,\r') 114 | outfiles["N"].write(' native_unit_of_measurement=UnitOfTemperature.KELVIN,\r') 115 | outfiles["N"].write(' feature=FEATURE_CODE_GEN\r') 116 | outfiles["N"].write(' ),\r') 117 | else: 118 | # time sensor... 119 | outfiles["D"].write(' ExtSensorEntityDescription(\r') 120 | outfiles["D"].write(f' key="{a_key}",\r') 121 | outfiles["D"].write(f' tag=WKHPTag.{a_key},\r') 122 | outfiles["D"].write(' device_class=SensorDeviceClass.DATE,\r') 123 | outfiles["D"].write(' native_unit_of_measurement=None,\r') 124 | outfiles["D"].write(' icon="mdi:clock-digital",\r') 125 | outfiles["D"].write(' entity_registry_enabled_default=False,\r') 126 | outfiles["D"].write(' feature=FEATURE_CODE_GEN\r') 127 | outfiles["D"].write(' ),\r') 128 | outfiles["S"].write(']\r') 129 | outfiles["N"].write(']\r') 130 | outfiles["D"].write(']\r') 131 | outfiles["S"].flush() 132 | outfiles["N"].flush() 133 | outfiles["D"].flush() 134 | 135 | with open("const_gen.py", 'w+') as out: 136 | out.write('BINARY_SENSORS_GENERATED: Final = []\r') 137 | out.write('NUMBER_SENSORS_GENERATED: Final = [\r') 138 | with open(files["N"], 'r') as f: 139 | out.write(f.read()) 140 | out.write('SELECT_SENSORS_GENERATED: Final []\r') 141 | out.write('SENSOR_SENSORS_GENERATED: Final = [\r') 142 | with open(files["D"], 'r') as f: 143 | out.write(f.read()) 144 | out.write('SWITCH_SENSORS_GENERATED: Final = [\r') 145 | with open(files["S"], 'r') as f: 146 | out.write(f.read()) 147 | out.flush() 148 | 149 | generateTags() 150 | generateEntityDesc() 151 | -------------------------------------------------------------------------------- /sample-view.yaml: -------------------------------------------------------------------------------- 1 | - theme: Backend-selected 2 | path: wkh 3 | icon: mdi:radiator 4 | badges: [] 5 | cards: 6 | - type: entities 7 | entities: 8 | - entity: select.wkh_enable_cooling 9 | name: Betrieb Kühlung 10 | - entity: select.wkh_enable_heating 11 | name: Betrieb Heizung 12 | - entity: select.wkh_enable_warmwater 13 | name: Betrieb Warmwasser 14 | - entity: switch.wkh_holiday_enabled 15 | name: Urlaubsfunktion 16 | - entity: sensor.wkh_holiday_start_time 17 | name: Urlaub beginnt am 18 | - entity: sensor.wkh_holiday_end_time 19 | name: Endet am 20 | title: Waterkotte 21 | show_header_toggle: false 22 | state_color: true 23 | - type: vertical-stack 24 | cards: 25 | - type: entities 26 | title: Status 27 | icon: mdi:list-status 28 | entities: 29 | - entity: binary_sensor.wkh_state_compressor 30 | name: Verdichter 31 | - entity: binary_sensor.wkh_state_heatingpump 32 | name: Wärmepumpe 33 | - entity: binary_sensor.wkh_state_sourcepump 34 | name: Quellpumpe 35 | - entity: binary_sensor.wkh_state_evd 36 | name: Überhitzungsregler 37 | - entity: binary_sensor.wkh_state_external_heater 38 | name: Heizstab 39 | - entity: binary_sensor.wkh_state_compressor2 40 | name: Verdichter II 41 | - entity: binary_sensor.wkh_state_water 42 | name: Warmwasser 43 | - entity: binary_sensor.wkh_status_heating 44 | name: Heizung 45 | - entity: binary_sensor.wkh_status_cooling 46 | name: Kühlung 47 | - entity: binary_sensor.wkh_status_water 48 | name: Warmwasser 49 | - entity: sensor.wkh_state_service 50 | name: Service? 51 | show_header_toggle: false 52 | state_color: true 53 | - type: entities 54 | title: Temperaturen 55 | icon: mdi:thermometer 56 | entities: 57 | - entity: sensor.wkh_temperature_outside 58 | name: Außen 59 | - entity: sensor.wkh_temperature_heating 60 | name: Heizung 61 | - entity: sensor.wkh_temperature_mix1 62 | name: Mischerkreis 1 63 | - entity: sensor.wkh_temperature_water 64 | name: Warmwasser 65 | - entity: sensor.wkh_temperature_buffertank 66 | name: Speicher 67 | show_header_toggle: false 68 | state_color: true 69 | - type: entities 70 | title: Leistung 71 | icon: mdi:lightning-bolt 72 | entities: 73 | - entity: sensor.wkh_power_electric 74 | name: Leistungsaufnahme 75 | - entity: sensor.wkh_cop_heating 76 | name: COP 77 | - entity: sensor.wkh_power_heating 78 | name: Thermische Leistung 79 | - entity: sensor.wkh_cop_cooling 80 | name: COP Kälteleistung 81 | - entity: sensor.wkh_power_cooling 82 | name: Kälteleistung 83 | show_header_toggle: false 84 | state_color: true 85 | - type: entities 86 | title: Heizung 87 | icon: mdi:radiator 88 | show_header_toggle: false 89 | state_color: true 90 | entities: 91 | - entity: select.wkh_temperature_heating_mode 92 | - entity: number.wkh_temperature_heating_setpoint 93 | name: Heiztemperatur [manuell] 94 | - entity: number.wkh_temperature_heating_adjust 95 | name: Anpassung 96 | icon: mdi:plus-minus-variant 97 | - entity: sensor.wkh_temperature_heating 98 | name: Istwert 99 | icon: mdi:thermometer 100 | - entity: sensor.wkh_temperature_heating_demand 101 | name: Sollwert 102 | icon: mdi:thermometer 103 | - entity: number.wkh_temperature_heating_hysteresis 104 | name: Schaltdifferenz Sollwert 105 | icon: mdi:delta 106 | - entity: number.wkh_temperature_heating_hc_limit 107 | name: Heizgrenze 108 | - entity: number.wkh_temperature_heating_hc_target 109 | name: Heizgrenze Soll 110 | - entity: number.wkh_temperature_heating_hc_outdoor_norm 111 | name: Norm-Außen 112 | - entity: number.wkh_temperature_heating_hc_norm 113 | name: Heizkreis Norm 114 | - type: entities 115 | title: Mischerkreis 1 116 | icon: mdi:numeric-1-circle 117 | entities: 118 | - entity: number.wkh_temperature_mix1_adjust 119 | name: Anpassung 120 | icon: mdi:plus-minus-variant 121 | - entity: sensor.wkh_temperature_mix1 122 | name: Istwert 123 | icon: mdi:thermometer 124 | - entity: sensor.wkh_temperature_mix1_demand 125 | name: Sollwert 126 | icon: mdi:thermometer 127 | - entity: number.wkh_temperature_mix1_hc_limit 128 | name: Heizgrenze 129 | - entity: number.wkh_temperature_mix1_hc_target 130 | name: Heizgrenze Soll 131 | - entity: number.wkh_temperature_mix1_hc_outdoor_norm 132 | name: Norm-Außen 133 | - entity: number.wkh_temperature_mix1_hc_heating_norm 134 | name: Heizkreis Norm 135 | - type: vertical-stack 136 | cards: 137 | - type: entities 138 | state_color: true 139 | title: Warmwasser 140 | show_header_toggle: false 141 | icon: mdi:water-thermometer 142 | entities: 143 | - entity: sensor.wkh_temperature_water 144 | name: Istwert 145 | icon: mdi:thermometer 146 | - entity: number.wkh_temperature_water_setpoint 147 | name: Sollwert 148 | icon: mdi:thermometer 149 | - entity: number.wkh_temperature_water_hysteresis 150 | name: Schaltdifferenz Sollwert 151 | icon: mdi:delta 152 | - type: entities 153 | state_color: true 154 | title: Desinfektion 155 | show_header_toggle: false 156 | icon: mdi:shield-bug 157 | entities: 158 | - entity: number.wkh_temperature_water_disinfection 159 | name: Temperatur 160 | icon: mdi:thermometer 161 | - entity: sensor.wkh_schedule_water_disinfection_start_time 162 | name: Startzeit 163 | - entity: number.wkh_schedule_water_disinfection_duration 164 | name: Dauer (in Stunden) 165 | - type: custom:multiple-entity-row 166 | entity: switch.wkh_schedule_water_disinfection_7su 167 | state_header: So 168 | toggle: true 169 | state_color: true 170 | entities: 171 | - entity: switch.wkh_schedule_water_disinfection_1mo 172 | name: Mo 173 | toggle: true 174 | state_color: true 175 | - entity: switch.wkh_schedule_water_disinfection_2tu 176 | name: Di 177 | toggle: true 178 | state_color: true 179 | - entity: switch.wkh_schedule_water_disinfection_3we 180 | name: Mi 181 | toggle: true 182 | state_color: true 183 | - entity: switch.wkh_schedule_water_disinfection_4th 184 | name: Do 185 | toggle: true 186 | state_color: true 187 | - entity: switch.wkh_schedule_water_disinfection_5fr 188 | name: Fr 189 | toggle: true 190 | state_color: true 191 | - entity: switch.wkh_schedule_water_disinfection_6sa 192 | name: Sa 193 | toggle: true 194 | state_color: true 195 | - type: entities 196 | title: Details 197 | entities: 198 | - entity: sensor.wkh_percent_compressor 199 | name: Leistung Verdichter 200 | - entity: sensor.wkh_percent_heat_circ_pump 201 | name: Drehzahl Heizungspumpe 202 | - entity: sensor.wkh_percent_source_pump 203 | name: Drehzahl Quellenpumpe 204 | - entity: sensor.wkh_position_expansion_valve 205 | name: EEV Ventilöffnung 206 | - entity: sensor.wkh_suction_gas_overheating 207 | name: Sauggas Überhitzung 208 | - entity: sensor.wkh_pressure_condensation 209 | name: Druck Kondensator 210 | - entity: sensor.wkh_temperature_condensation 211 | name: Temp. Kondensator 212 | - entity: sensor.wkh_pressure_evaporation 213 | name: Druck Verdampfer 214 | - entity: sensor.wkh_temperature_evaporation 215 | name: Temp. Verdampfer 216 | - entity: sensor.wkh_temperature_flow 217 | name: Temp. Vorlauf 218 | - entity: sensor.wkh_temperature_return 219 | name: Temp. Rücklauf 220 | - entity: sensor.wkh_temperature_source_entry 221 | name: Temp. Quelle Eingang 222 | - entity: sensor.wkh_temperature_source_exit 223 | name: Temp. Quelle Ausgang 224 | - entity: sensor.wkh_temperature_suction_line 225 | name: Temp. Saugleitung -------------------------------------------------------------------------------- /custom_components/waterkotte_heatpump/strings.json: -------------------------------------------------------------------------------- 1 | { 2 | "entity": { 3 | "binary_sensor": { 4 | "state_sourcepump": {"name": "Source pump"}, 5 | "state_heatingpump": {"name": "Heat pump"}, 6 | "state_evd": {"name": "EVD"}, 7 | "state_compressor": {"name": "Compressor"}, 8 | "state_compressor2": {"name": "Compressor II"}, 9 | "state_external_heater": {"name": "Electrical heater"}, 10 | "state_alarm": {"name": "Notifications"}, 11 | "state_cooling": {"name": "Cooling"}, 12 | "state_water": {"name": "Hot water"}, 13 | "state_pool": {"name": "Pool"}, 14 | "state_solar": {"name": "Solar"}, 15 | "state_cooling4way": {"name": "4-way valve"}, 16 | "status_heating": {"name": "Heating mode"}, 17 | "status_water": {"name": "Hot water mode"}, 18 | "status_cooling": {"name": "Cooling mode"}, 19 | "status_pool": {"name": "Pool mode"}, 20 | "status_solar": {"name": "Solar mode"}, 21 | "status_heating_circulation_pump": {"name": "Circulation pump heating mode"}, 22 | "status_solar_circulation_pump": {"name": "Circulation pump solar mode"}, 23 | "status_buffer_tank_circulation_pump": {"name": "Circulation pump buffer tank mode"}, 24 | "status_compressor": {"name": "Compressor mode"}, 25 | "state_blocking_time": {"name": "Blocking time"}, 26 | "state_test_run": {"name": "Test run"}, 27 | "state_heating_circulation_pump_d425": {"name": "Circulation pump heating"}, 28 | "state_buffertank_circulation_pump_d377": {"name": "Circulation pump buffer tank"}, 29 | "state_pool_circulation_pump_d549": {"name": "Circulation pump pool"}, 30 | "state_mix1_circulation_pump_d248": {"name": "Circulation pump mixer 1"}, 31 | "state_mix2_circulation_pump_d291": {"name": "Circulation pump mixer 2"}, 32 | "state_mix3_circulation_pump_d334": {"name": "Circulation pump mixer 3"}, 33 | "state_mix1_circulation_pump_d563": {"name": "Circulation pump mixer 1 [D563]"} 34 | }, 35 | "number": { 36 | "temperature_return_setpoint": {"name": "T setpoint"}, 37 | "temperature_cooling_setpoint": {"name": "T Cooling"}, 38 | "temperature_cooling_outdoor_limit": {"name": "T out begin"}, 39 | "temperature_cooling_flow_limit": {"name": "Flow temperature limitation"}, 40 | "temperature_heating_setpoint": {"name": "Heating temperature"}, 41 | "temperature_heating_adjust": {"name": "Temperature adjustment"}, 42 | "temperature_heating_hysteresis": {"name": "Hysteresis setpoint"}, 43 | "temperature_mix1_adjust": {"name": "Temperature adjustment"}, 44 | "temperature_mix2_adjust": {"name": "Temperature adjustment"}, 45 | "temperature_mix3_adjust": {"name": "Temperature adjustment"}, 46 | "temperature_heating_hc_limit": {"name": "T heating limit"}, 47 | "temperature_heating_hc_target": {"name": "T heating limit target"}, 48 | "temperature_heating_hc_outdoor_norm": {"name": "T norm outdoor"}, 49 | "temperature_heating_hc_norm": {"name": "T norm heating circle"}, 50 | "temperature_heating_setpointlimit_max": {"name": "Limit for setpoint (Max.)"}, 51 | "temperature_heating_setpointlimit_min": {"name": "Limit for setpoint (Min.)"}, 52 | "temperature_water_setpoint": {"name": "Demanded temperature"}, 53 | "temperature_water_hysteresis": {"name": "Hysteresis setpoint"}, 54 | "temperature_mix1_hc_limit": {"name": "T heating limit"}, 55 | "temperature_mix1_hc_target": {"name": "T heating limit target"}, 56 | "temperature_mix1_hc_outdoor_norm": {"name": "T norm outdoor"}, 57 | "temperature_mix1_hc_heating_norm": {"name": "T norm heating circle"}, 58 | "temperature_mix1_hc_max": {"name": "Max. temperature in mixing circle"}, 59 | "temperature_mix2_hc_limit": {"name": "T heating limit"}, 60 | "temperature_mix2_hc_target": {"name": "T heating limit target"}, 61 | "temperature_mix2_hc_outdoor_norm": {"name": "T norm outdoor"}, 62 | "temperature_mix2_hc_heating_norm": {"name": "T norm heating circle"}, 63 | "temperature_mix2_hc_max": {"name": "Max. temperature in mixing circle"}, 64 | "temperature_mix3_hc_limit": {"name": "T heating limit"}, 65 | "temperature_mix3_hc_target": {"name": "T heating limit target"}, 66 | "temperature_mix3_hc_outdoor_norm": {"name": "T norm outdoor"}, 67 | "temperature_mix3_hc_heating_norm": {"name": "T norm heating circle"}, 68 | "temperature_mix3_hc_max": {"name": "Max. temperature in mixing circle"}, 69 | "temperature_water_disinfection": {"name": "Demanded temperature"}, 70 | "schedule_water_disinfection_duration": {"name": "Max. runtime"}, 71 | "temperature_pool_setpoint": {"name": "Demanded temperature"}, 72 | "temperature_pool_hysteresis": {"name": "Hysteresis setpoint"}, 73 | "temperature_pool_hc_limit": {"name": "T heating limit"}, 74 | "temperature_pool_hc_target": {"name": "T heating limit target"}, 75 | "temperature_pool_hc_outdoor_norm": {"name": "T norm outdoor"}, 76 | "temperature_pool_hc_norm": {"name": "T norm heating circle"} 77 | }, 78 | "select": { 79 | "temperature_heating_mode": { 80 | "name": "Heating Control", 81 | "state": { 82 | "hm0": "Weather-compensated", 83 | "hm1": "Manual Setpoint", 84 | "hm2": "Setpoint BMS", 85 | "hm3": "Setpoint EXT", 86 | "hm4": "Setpoint 0-10V", 87 | "hm5": "Based on Mixing circle" 88 | } 89 | }, 90 | "enable_cooling": {"name": "Operation mode cooling"}, 91 | "enable_heating": {"name": "Operation mode heating"}, 92 | "enable_pv": {"name": "Operation mode PV"}, 93 | "enable_warmwater": {"name": "Operation mode hot water"}, 94 | "enable_pool": {"name": "Operation mode pool"}, 95 | "enable_external_heater": {"name": "Operation mode external heater"}, 96 | "enable_mixing1": {"name": "Operation mode Mixer 1"}, 97 | "enable_mixing2": {"name": "Operation mode Mixer 2"}, 98 | "enable_mixing3": {"name": "Operation mode Mixer 3"} 99 | }, 100 | "sensor": { 101 | "energy_consumption_total_year": {"name": "Electrical year performance"}, 102 | "compressor_electric_consumption_year": {"name": "Compressor year performance"}, 103 | "sourcepump_electric_consumption_year": {"name": "Heat source pump year performance"}, 104 | "electrical_heater_electric_consumption_year": {"name": "Electrical heater year performance"}, 105 | "energy_production_total_year": {"name": "Thermal year performance"}, 106 | "heating_energy_production_year": {"name": "Heating year performance"}, 107 | "hot_water_energy_production_year": {"name": "Hot water year performance"}, 108 | "pool_energy_production_year": {"name": "Pool year performance"}, 109 | "cooling_energy_year": {"name": "Cooling year performance"}, 110 | "temperature_outside": {"name": "Outdoor temperature"}, 111 | "temperature_outside_1h": {"name": "Outdoor temperature 1h"}, 112 | "temperature_outside_24h": {"name": "Outdoor temperature 24h"}, 113 | "temperature_source_entry": {"name": "T source entry"}, 114 | "temperature_source_exit": {"name": "T source exit"}, 115 | "temperature_evaporation": {"name": "T evaporation"}, 116 | "temperature_suction_line": {"name": "T suction line"}, 117 | "temperature_return": {"name": "T return"}, 118 | "temperature_flow": {"name": "T flow"}, 119 | "temperature_condensation": {"name": "T condensation"}, 120 | "temperature_buffertank": {"name": "Temperature buffer tank"}, 121 | "temperature_room": {"name": "Room temperature"}, 122 | "temperature_room_1h": {"name": "Room temperature 1h"}, 123 | "temperature_heating": {"name": "Actual temperature"}, 124 | "temperature_heating_demand": {"name": "Demanded temperature"}, 125 | "temperature_cooling": {"name": "Actual temperature"}, 126 | "temperature_cooling_demand": {"name": "Actual temperature"}, 127 | "temperature_water": {"name": "Hot water temperature"}, 128 | "temperature_water_demand": {"name": "Demanded temperature"}, 129 | "temperature_mix1": {"name": "Actual temperature"}, 130 | "temperature_mix1_percent": {"name": "Y"}, 131 | "temperature_mix1_demand": {"name": "Demanded temperature"}, 132 | "temperature_mix2": {"name": "Actual temperature"}, 133 | "temperature_mix2_percent": {"name": "Y"}, 134 | "temperature_mix2_demand": {"name": "Demanded temperature"}, 135 | "temperature_mix3": {"name": "Actual temperature"}, 136 | "temperature_mix3_percent": {"name": "Y"}, 137 | "temperature_mix3_demand": {"name": "Demanded temperature"}, 138 | "temperature_pool": {"name": "Actual temperature"}, 139 | "temperature_pool_demand": {"name": "Demanded temperature"}, 140 | "temperature_solar": {"name": "T Solar"}, 141 | "temperature_solar_exit": {"name": "Exit temperature solar collector"}, 142 | "temperature_discharge": {"name": "Discharge temperature"}, 143 | "pressure_evaporation": {"name": "p evaporation"}, 144 | "pressure_condensation": {"name": "p condensation"}, 145 | "pressure_water": {"name": "Water pressure"}, 146 | "position_expansion_valve": {"name": "Valve opening EEV"}, 147 | "suction_gas_overheating": {"name": "suction gas overheating"}, 148 | "power_electric": {"name": "Electrical power"}, 149 | "power_heating": {"name": "Thermal power"}, 150 | "power_cooling": {"name": "Cooling power"}, 151 | "cop_heating": {"name": "COP"}, 152 | "cop_cooling": {"name": "COP cooling power"}, 153 | "percent_heat_circ_pump": {"name": "Speed heating pump"}, 154 | "percent_source_pump": {"name": "Speed source pump"}, 155 | "percent_compressor": {"name": "Power compressor"}, 156 | "holiday_start_time": {"name": "Holiday start"}, 157 | "holiday_end_time": {"name": "Holiday end"}, 158 | "schedule_water_disinfection_start_time": {"name": "Start time"}, 159 | "state_service": {"name": "Service data"} 160 | }, 161 | "switch": { 162 | "holiday_enabled": {"name": "Holiday"}, 163 | "schedule_water_disinfection_1mo": {"name": "Monday"}, 164 | "schedule_water_disinfection_2tu": {"name": "Tuesday"}, 165 | "schedule_water_disinfection_3we": {"name": "Wednesday"}, 166 | "schedule_water_disinfection_4th": {"name": "Thursday"}, 167 | "schedule_water_disinfection_5fr": {"name": "Friday"}, 168 | "schedule_water_disinfection_6sa": {"name": "Saturday"}, 169 | "schedule_water_disinfection_7su": {"name": "Sunday"}, 170 | "permanent_heating_circulation_pump_winter_d1103": {"name": "Continuous operation heating pump during heating period"}, 171 | "permanent_heating_circulation_pump_summer_d1104": {"name": "Continuous operation heating pump during cooling period"} 172 | } 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /custom_components/waterkotte_heatpump/config_flow.py: -------------------------------------------------------------------------------- 1 | """Adds config flow for Waterkotte Heatpump.""" 2 | import logging 3 | import voluptuous as vol 4 | 5 | from homeassistant import config_entries 6 | from homeassistant.core import callback 7 | from homeassistant.helpers.aiohttp_client import async_create_clientsession 8 | from homeassistant.helpers import selector 9 | from homeassistant.util import uuid as uuid_util 10 | 11 | from homeassistant.const import CONF_ID, CONF_HOST, CONF_USERNAME, CONF_PASSWORD 12 | 13 | from .const import ( 14 | DOMAIN, 15 | TITLE, 16 | CONF_POLLING_INTERVAL, 17 | CONF_TAGS_PER_REQUEST, 18 | CONF_BIOS, 19 | CONF_FW, 20 | CONF_SERIAL, 21 | CONF_SERIES, 22 | CONF_SYSTEMTYPE, 23 | CONF_ADD_SCHEDULE_ENTITIES, 24 | CONF_ADD_SERIAL_AS_ID, 25 | CONF_USE_DISINFECTION, 26 | CONF_USE_HEATING_CURVE, 27 | CONF_USE_VENT, 28 | CONF_USE_POOL 29 | ) 30 | 31 | from custom_components.waterkotte_heatpump.pywaterkotte_ha import WaterkotteClient 32 | from custom_components.waterkotte_heatpump.pywaterkotte_ha.const import EASYCON, ECOTOUCH 33 | from custom_components.waterkotte_heatpump.pywaterkotte_ha.tags import WKHPTag 34 | from .pywaterkotte_ha.error import Http404Exception 35 | 36 | _LOGGER: logging.Logger = logging.getLogger(__package__) 37 | 38 | 39 | class WaterkotteHeatpumpFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): 40 | """Config flow for waterkotte_heatpump.""" 41 | 42 | VERSION = 1 43 | CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_POLL 44 | 45 | def __init__(self): 46 | """Initialize.""" 47 | self._errors = {} 48 | self._user_step_user_input = None 49 | self._bios = "" 50 | self._firmware = "" 51 | self._id = "" 52 | self._series = "" 53 | self._serial = "" 54 | 55 | async def async_step_user(self, user_input=None): 56 | """Handle a flow initialized by the user.""" 57 | self._errors = {} 58 | 59 | if user_input is not None: 60 | if CONF_SYSTEMTYPE in user_input: 61 | # it really sucks, that translation keys have to be lower case... 62 | user_input[CONF_SYSTEMTYPE] = user_input[CONF_SYSTEMTYPE].upper() 63 | 64 | if user_input[CONF_SYSTEMTYPE] == EASYCON: 65 | return await self.async_step_user_easycon() 66 | else: 67 | return await self.async_step_user_ecotouch() 68 | else: 69 | user_input = {} 70 | user_input[CONF_SYSTEMTYPE] = ECOTOUCH 71 | 72 | # it really sucks, that translation keys have to be lower case... so we need to make sure that our 73 | # options are all translate to lower case! 74 | return self.async_show_form( 75 | step_id="user", 76 | data_schema=vol.Schema({ 77 | vol.Required(CONF_SYSTEMTYPE, default=(user_input.get(CONF_SYSTEMTYPE, ECOTOUCH)).lower()): 78 | selector.SelectSelector( 79 | selector.SelectSelectorConfig( 80 | options=[ECOTOUCH.lower(), EASYCON.lower()], 81 | mode=selector.SelectSelectorMode.DROPDOWN, 82 | translation_key=CONF_SYSTEMTYPE 83 | ) 84 | ), 85 | }), 86 | last_step=False, 87 | errors=self._errors 88 | ) 89 | 90 | async def async_step_user_easycon(self, user_input=None): 91 | """Handle a flow initialized by the user.""" 92 | self._errors = {} 93 | 94 | # Uncomment the next 2 lines if only a single instance of the integration is allowed: 95 | # if self._async_current_entries(): 96 | # return self.async_abort(reason="single_instance_allowed") 97 | 98 | if user_input is not None: 99 | user_input[CONF_SYSTEMTYPE] = EASYCON 100 | user_input[CONF_ADD_SCHEDULE_ENTITIES] = False 101 | valid = await self._test_credentials( 102 | host=user_input[CONF_HOST], 103 | username=None, 104 | pwd=None, 105 | system_type=user_input[CONF_SYSTEMTYPE], 106 | tags_per_request=user_input[CONF_TAGS_PER_REQUEST], 107 | ) 108 | if valid: 109 | user_input[CONF_BIOS] = self._bios 110 | user_input[CONF_FW] = self._firmware 111 | user_input[CONF_SERIES] = self._series 112 | user_input[CONF_SERIAL] = self._serial 113 | user_input[CONF_ID] = self._id 114 | self._user_step_user_input = dict(user_input) 115 | return await self.async_step_features() 116 | else: 117 | self._errors["base"] = "type" 118 | else: 119 | user_input = {} 120 | user_input[CONF_HOST] = "" 121 | user_input[CONF_ADD_SERIAL_AS_ID] = False 122 | 123 | return self.async_show_form( 124 | step_id="user_easycon", 125 | data_schema=vol.Schema({ 126 | vol.Required(CONF_HOST, default=user_input.get(CONF_HOST)): str, 127 | vol.Required(CONF_POLLING_INTERVAL, default=500): int, 128 | vol.Required(CONF_TAGS_PER_REQUEST, default=25): int, 129 | vol.Required(CONF_ADD_SERIAL_AS_ID, default=False): bool 130 | }), 131 | last_step=False, 132 | errors=self._errors 133 | ) 134 | 135 | async def async_step_user_ecotouch(self, user_input=None): 136 | """Handle a flow initialized by the user.""" 137 | self._errors = {} 138 | 139 | # Uncomment the next 2 lines if only a single instance of the integration is allowed: 140 | # if self._async_current_entries(): 141 | # return self.async_abort(reason="single_instance_allowed") 142 | 143 | if user_input is not None: 144 | user_input[CONF_SYSTEMTYPE] = ECOTOUCH 145 | valid = await self._test_credentials( 146 | host=user_input[CONF_HOST], 147 | username=user_input[CONF_USERNAME], 148 | pwd=user_input[CONF_PASSWORD], 149 | system_type=user_input[CONF_SYSTEMTYPE], 150 | tags_per_request=user_input[CONF_TAGS_PER_REQUEST], 151 | ) 152 | if valid: 153 | user_input[CONF_BIOS] = self._bios 154 | user_input[CONF_FW] = self._firmware 155 | user_input[CONF_SERIES] = self._series 156 | user_input[CONF_SERIAL] = self._serial 157 | user_input[CONF_ID] = self._id 158 | self._user_step_user_input = dict(user_input) 159 | return await self.async_step_features() 160 | else: 161 | self._errors["base"] = "auth" 162 | else: 163 | user_input = {} 164 | user_input[CONF_HOST] = "" 165 | user_input[CONF_USERNAME] = "waterkotte" 166 | user_input[CONF_PASSWORD] = "waterkotte" 167 | user_input[CONF_ADD_SCHEDULE_ENTITIES] = False 168 | user_input[CONF_ADD_SERIAL_AS_ID] = False 169 | 170 | return self.async_show_form( 171 | step_id="user_ecotouch", 172 | data_schema=vol.Schema({ 173 | vol.Required(CONF_HOST, default=user_input.get(CONF_HOST)): str, 174 | vol.Optional(CONF_USERNAME, default=user_input.get(CONF_USERNAME)): str, 175 | vol.Required(CONF_PASSWORD, default=user_input.get(CONF_PASSWORD)): str, 176 | vol.Required(CONF_POLLING_INTERVAL, default=60): int, 177 | vol.Required(CONF_TAGS_PER_REQUEST, default=75): int, 178 | vol.Required(CONF_ADD_SCHEDULE_ENTITIES, default=False): bool, 179 | vol.Required(CONF_ADD_SERIAL_AS_ID, default=False): bool, 180 | }), 181 | last_step=False, 182 | errors=self._errors 183 | ) 184 | 185 | async def async_step_features(self, user_input=None): 186 | self._errors = {} 187 | if user_input is not None: 188 | for k, v in user_input.items(): 189 | self._user_step_user_input[k] = v 190 | 191 | return self.async_create_entry(title=TITLE, data=self._user_step_user_input) 192 | else: 193 | return self.async_show_form( 194 | step_id="features", 195 | data_schema=vol.Schema({ 196 | vol.Required(CONF_USE_VENT, default=False): bool, 197 | vol.Required(CONF_USE_HEATING_CURVE, default=False): bool, 198 | vol.Required(CONF_USE_DISINFECTION, default=False): bool, 199 | vol.Required(CONF_USE_POOL, default=False): bool, 200 | }), 201 | last_step=True, 202 | errors=self._errors 203 | ) 204 | 205 | async def _test_credentials(self, host, username, pwd, system_type, tags_per_request): 206 | try: 207 | session = async_create_clientsession(self.hass) 208 | client = WaterkotteClient(host=host, username=username, pwd=pwd, system_type=system_type, 209 | web_session=session, tags=None, tags_per_request=tags_per_request, 210 | lang=self.hass.config.language.lower()) 211 | await client.login() 212 | init_tags = [ 213 | WKHPTag.VERSION_BIOS, 214 | WKHPTag.VERSION_CONTROLLER, 215 | WKHPTag.INFO_ID, 216 | WKHPTag.INFO_SERIAL, 217 | WKHPTag.INFO_SERIES, 218 | ] 219 | ret = await client.async_read_values(init_tags) 220 | 221 | self._bios = ret[WKHPTag.VERSION_BIOS]["value"] 222 | self._firmware = ret[WKHPTag.VERSION_CONTROLLER]["value"] 223 | self._id = str(ret[WKHPTag.INFO_ID]["value"]) 224 | self._series = str(ret[WKHPTag.INFO_SERIES]["value"]) 225 | self._serial = str(ret[WKHPTag.INFO_SERIAL]["value"]) 226 | if self._serial is None or self._serial == "None": 227 | self._serial = uuid_util.random_uuid_hex() 228 | 229 | _LOGGER.info(f"successfully validated login -> result: {ret}") 230 | return True 231 | 232 | except Exception as exc: 233 | if isinstance(exc, Http404Exception): 234 | _LOGGER.error(f"EASYCON Mode caused HTTP 404") 235 | else: 236 | _LOGGER.error(f"Exception while test credentials: {exc}") 237 | return False 238 | 239 | @staticmethod 240 | @callback 241 | def async_get_options_flow(config_entry): 242 | return WaterkotteHeatpumpOptionsFlowHandler(config_entry) 243 | 244 | 245 | class WaterkotteHeatpumpOptionsFlowHandler(config_entries.OptionsFlow): 246 | def __init__(self, config_entry): 247 | """Initialize HACS options flow.""" 248 | if len(dict(config_entry.options)) == 0: 249 | self.options = dict(config_entry.data) 250 | else: 251 | self.options = dict(config_entry.options) 252 | 253 | async def async_step_init(self, user_input=None): # pylint: disable=unused-argument 254 | """Manage the options.""" 255 | return await self.async_step_user() 256 | 257 | async def async_step_user(self, user_input=None): 258 | """Handle a flow initialized by the user.""" 259 | if user_input is not None: 260 | self.options.update(user_input) 261 | return await self._update_options() 262 | 263 | return self.async_show_form( 264 | step_id="user", 265 | data_schema=vol.Schema({ 266 | vol.Optional(CONF_USERNAME, default=self.options.get(CONF_USERNAME, "waterkotte")): str, 267 | vol.Required(CONF_PASSWORD, default=self.options.get(CONF_PASSWORD, "waterkotte")): str, 268 | vol.Required(CONF_POLLING_INTERVAL, default=self.options.get(CONF_POLLING_INTERVAL, 60)): int, 269 | vol.Required(CONF_TAGS_PER_REQUEST, default=self.options.get(CONF_TAGS_PER_REQUEST, 75)): int, 270 | vol.Required(CONF_ADD_SCHEDULE_ENTITIES, default=self.options.get(CONF_ADD_SCHEDULE_ENTITIES, False)): bool 271 | }), 272 | ) 273 | 274 | async def _update_options(self): 275 | return self.async_create_entry(title=TITLE, data=self.options) 276 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Home Assistant Integration for Waterkotte Heatpumps [+2020] 2 | 3 | ![logo](https://github.com/marq24/ha-waterkotte/raw/main/logo.png) 4 | 5 | This Home Assistant Integration is providing information from the German heatpump pioneer Waterkotte. In addition and where possible functions are provided to control the system. 6 | 7 | __Please note__, _that this integration is not official and not supported by the Waterkotte development team. I am not affiliated with Waterkotte in any way._ 8 | 9 | All data will be fetched (or send) to your Waterkotte via the build in webserver of the unit. So the functionality is based on the data and settings that are available also via the frontend that you can directly access via a web-browser. 10 | 11 | [![hacs_badge][hacsbadge]][hacs] [![github][ghsbadge]][ghs] [![BuyMeCoffee][buymecoffeebadge]][buymecoffee] [![PayPal][paypalbadge]][paypal] [![hainstall][hainstallbadge]][hainstall] 12 | 13 | ## This component will set up the following platforms 14 | 15 | | Platform | Description | 16 | |-----------------|------------------------------------------------------| 17 | | `binary_sensor` | Show something `True` or `False`. | 18 | | `sensor` | Show info from Waterkotte Heatpump API. | 19 | | `switch` | Switch something `True` or `False`. | 20 | | `select` | Select a value from options. | 21 | | `number` | adjustable Temperatures (demanded or heating curves) | 22 | | `service` | Provides services to interact with heatpump | 23 | 24 | ## Disclaimer 25 | 26 | Please be aware, that we are developing this integration to best of our knowledge and belief, but cant give a guarantee. Therefore, use this integration **at your own risk**. 27 | 28 | ## What you can get [with Version 2024.3.0 (or higher)] 29 | 30 | [![sampleview](https://github.com/marq24/ha-waterkotte/raw/main/sample-view-s.png)](https://github.com/marq24/ha-waterkotte/raw/main/sample-view.png) 31 | 32 | [[Get the sources for the sample dashboard_above](https://github.com/marq24/ha-waterkotte/blob/main/sample-view.yaml)] - Please note, that this sample dashboard makes use of the custom [multiple-entity-row](https://github.com/benct/lovelace-multiple-entity-row) frontend integration that need to be installed separately. 33 | 34 | ## Setup / Installation 35 | if you have installed the previous version of the waterkotte integration from me (marq24) - please [follow the migration guide](https://github.com/marq24/ha-waterkotte/blob/main/README.md#migration). 36 | 37 | ### Step I: Install the integration 38 | 39 | #### Option 1: via HACS 40 | [![Open your Home Assistant instance and adding repository to HACS.](https://my.home-assistant.io/badges/hacs_repository.svg)](https://my.home-assistant.io/redirect/hacs_repository/?owner=marq24&repository=ha-waterkotte&category=integration) 41 | 42 | - Install [Home Assistant Community Store (HACS)](https://hacs.xyz/) 43 | - Add integration repository (search for "Waterkotte Heatpump [+2020]" in "Explore & Download Repositories") 44 | - Use the 3-dots at the right of the list entry (not at the top bar!) to download/install the custom integration - the latest release version is automatically selected. Only select a different version if you have specific reasons. 45 | - After you presses download and the process has completed, you must __Restart Home Assistant__ to install all dependencies 46 | - Setup the custom integration as described below (see _Step II: Adding or enabling the integration_) 47 | 48 | #### Option 2: manual steps 49 | 50 | - Copy all files from `custom_components/waterkotte_heatpump/` to `custom_components/waterkotte_heatpump/` inside your config Home Assistant directory. 51 | - Restart Home Assistant to install all dependencies 52 | 53 | ### Step II: Adding or enabling the integration 54 | 55 | __You must have installed the integration (manually or via HACS before)!__ 56 | 57 | #### Option 1: My Home Assistant (2021.3+) 58 | 59 | Just click the following Button to start the configuration automatically (for the rest see _Option 2: Manually steps by step_): 60 | 61 | [![Open your Home Assistant instance and start setting up a new integration.](https://my.home-assistant.io/badges/config_flow_start.svg)](https://my.home-assistant.io/redirect/config_flow_start/?domain=waterkotte) 62 | 63 | #### Option 2: Manually steps by step 64 | 65 | Use the following steps for a manual configuration by adding the custom integration using the web interface and follow instruction on screen: 66 | 67 | - Go to `Configuration -> Integrations` and add "Waterkotte" integration 68 | - Provide the IP address (or hostname) of your Waterkotte Heatpump web server 69 | - Select the Interface-Type of your Waterkotte (see table below) 70 | - Select the number of TAGs that can be fetched in a single call to your device (older devices might need to adjust this value - for my in 2022 installed Waterkotte 75 is totally fine) 71 | - Provide area where the heatpump is located 72 | 73 | #### General additional notes 74 | 75 | After the integration was added you can use the 'config' button to adjust your settings and you can additionally modify the update intervall 76 | 77 | Please note, that most of the available sensors are __not__ enabled by default. 78 | 79 | #### EcoTouch or EasyCon Mode - How to decide? 80 | 81 | Please take a look at the different login options and compare with your waterkotte in order to decide, what mode you must select for the integration (sorry only german example screens here) 82 | 83 | | EcoTouch | EasyCon | 84 | | --- |--------------------------------------------------------------------------------------------| 85 | | web login form | browser basic-auth | 86 | | | | 87 | 88 | __Don't get confused!__ The EcoTouch web login for newer Waterkotte models shows the text _EasyCon_ - but when there is a webpage where you must enter the login credentials, then you __must select the EcoTouch__ Mode for this integration! 89 | 90 | ## Services 91 | 92 | The Integration provides currently 5 services: 93 | 94 | ### Setting dates & times 95 | 96 | #### SET_HOLIDAY 97 | To set the times for the holiday mode use the provided service `waterkotte_heatpump.set_holiday` and set `start` and `end` parameter. 98 | 99 | #### SET_DISINFECTION_START_TIME 100 | To set the water disinfection start time (HH:MM) use the provided service `waterkotte_heatpump.set_disinfection_start_time` and set `starthhmm` parameter (seconds will be ignored). 101 | 102 | #### SET_SCHEDULE 103 | 104 | When using the service, first select the schedule (type) you want to adjust [Heating, Cooling, Hot Water, Mixer 1-3, Pool, Buffer Tank Circulation Pump, Solar Control, Photovoltaic], select then the __start time__ and the __end time__, __enable/disable__ the schedule and select the __days__ you would apply the setting. 105 | 106 | Additionally, it's possible to specify the __Adjustment I__ and the __Adjustment II__ options. Please be a bit patient when using the Service since there are approx. 100 different tags that have to be written to the heatpump when you apply adjustments for all 7 days. 107 | 108 | Please note also, that I did not find a way (yet) to load the current values of the entities into the 'Set a Schedule' dialog. So when adjusting the values via the service you do not see the current values of the fields. 109 | 110 | ### Get Energy Balance 111 | 112 | #### GET_ENERGY_BALANCE 113 | Retrieves the overall energy consumption data for the year 114 | 115 | #### GET_ENERGY_BALANCE_MONTHLY 116 | Retrieves the monthly breakdown energy consumption data for a moving 12 month window. 1 = January, 2 = February, etc... 117 | 118 | 119 | 120 | ## Waterkotte schedule adjustment support 121 | 122 | ### Introduction 123 | 124 | With this the integration it will be possible to adjust the Waterkotte Schedules for: 125 | - Heating 126 | - Cooling 127 | - Hot Water 128 | - Mixer 1, Mixer 2 & Mixer 3 129 | - Pool 130 | - Buffer Tank Circulation Pump 131 | - Solar Control (without adjustment I & adjustment II) 132 | - Photovoltaic (without adjustment I & adjustment II) 133 | 134 | The easiest way to adjust a schedule is via the 'Set Schedule' Service that can be found in your HA installation. Only via the service it's possible to adjust the start and end times. 135 | 136 | When you want to use/display schedule settings in your HA dashboards or use them in your automations you must enable the optional schedule entities [in the configuration of the integration]. __But be smart__ - only add these additional entities if you really need them. If they are added once it's quite tricky to get rid of them again. Please read further to get additional information about the amount of additional schedule entities that will be added to your HA installation. 137 | 138 | ### Calculating the amount of additional entities 139 | 140 | For each of the Schedules there are per __day__: 141 | 1. One __switch__ to turn ON/OFF the schedule 142 | 2. Two __switches__ to turn ON/OFF adjustment I & II 143 | 3. Two __values__ for each of the adjustments (+/- 10°K) 144 | 4. Three __start times__ (one for the schedule, and two for the adjustments) 145 | 5. Three __end times__ (one for the schedule, and two for the adjustments) 146 | 147 | This makes a total of 11 Sensor-Entities per day - each Schedule consist obviously of 7 days - so for each of the schedules above 77 Sensor-Entities will be available (even if added - all are disabled by default). 148 | 149 | This will result in __a total of 659__ additional (new) Sensor-Entities in order to support all Schedules - yes this is not a typo! __SIX HUNDRED FIFTY-NINE__! 150 | 151 | So please __only add the additional sensors__ if the use of the 'set schedule service' __is not sufficient for your use case.__ The service can make all the adjustments to your Waterkotte schedules, __without the need of having the additional sensor entities added__. 152 | 153 | ## Migration Guide 154 | 155 | This is the new version of the previous 'ha-waterkotte' repository (which have now been renamed to [`ha-waterkotte-the-fork`](https://github.com/marq24/ha-waterkotte-the-fork)). After the refactoring process have been completed, I have decided to create an independent repository - since the refactored version does not have much in common with the origin sources. 156 | 157 | Unfortunately HACS does not 'like' renaming of repositories, so you have to perform few steps in order to upgrade your home assistant installation to the latest ha-waterkotte integration version - sorry for this inconvenience! 158 | 159 | ### How to migrate to the new integration version 160 | 1. make a backup (just in case) 161 | 2. go to HACS menu of your home assistant installation 162 | 3. remove the (old) custom HACS repository 'https://github.com/marq24/ha-waterkotte' 163 | 164 | (This step will/should remove the Waterkotte Integration entry from the list of installed HACS Integrations) 165 | 5. add the __new__ repository 'https://github.com/marq24/ha-waterkotte' to HACS 166 | 6. install the waterkotte integration to your local HACS 167 | 7. restart your home assistant system 168 | 169 | YES - this procedure sounds *totally* silly - but HACS stores a custom-id for each repository - And since I have decided to rename the old repository which base on the work from pattisonmichael to 'https://github.com/marq24/ha-waterkotte-the-fork' and created an independent repository, this procedure is necessary in order to be notified about any future updates. 170 | 171 | ## Troubleshooting 172 | 173 | ### Sessions 174 | 175 | The Heatpump only allows 2 sessions and there is no way to close a session. Sometimes you will get an error about the login. Just wait a few minutes and it should autocorrect itself. Session usually time out within about 5 min. 176 | 177 | ### Stale Data 178 | 179 | The Heatpump will not always respond with data. This happens usually after the system changes status, e.g. start/stop the heating. There is not much we can do about this, unfortunately. I try to cache the data in possible for a better UX. 180 | 181 | ## Credits & Kudos 182 | 183 | | who | what | 184 | |--------------------------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| 185 | | [@pattisonmichael](https://github.com/pattisonmichael) | This project was initially forked from [Waterkotte-Integration](https://github.com/pattisonmichael/waterkotte-integration) by pattisonmichael (but both projects drifted apart over time - so this repo is now independent). | 186 | | [@chboland](https://github.com/chboland) | Christian Boland created a Python Waterkotte library [https://github.com/chboland/pywaterkotte](https://github.com/chboland/pywaterkotte) which was forked by [@pattisonmichael pywatterkotte library](https://github.com/pattisonmichael/pywaterkotte), so this integration is also based on the work from @chboland. | 187 | | [@oncleben31](https://github.com/oncleben31) | The forked original project was generated via the [Home Assistant Custom Component Cookiecutter](https://github.com/oncleben31/cookiecutter-homeassistant-custom-component) template. | 188 | | [@Ludeeus](https://github.com/ludeeus) | The forked original code template was mainly taken from the [integration_blueprint](https://github.com/custom-components/integration_blueprint) template | 189 | 190 | --- 191 | 192 | ###### Advertisement / Werbung - alternative way to support me 193 | 194 | ### Switch to Tibber! 195 | 196 | Be smart switch to Tibber - that's what I did in october 2023. If you want to join Tibber (become a customer), you might want to use my personal invitation link. When you use this link, Tibber will grant you and me a bonus of 50,-€ for each of us. This bonus then can be used in the Tibber store (not for your power bill) - e.g. to buy a Tibber Bridge. If you are already a Tibber customer and have not used an invitation link yet, you can also enter one afterward in the Tibber App (up to 14 days). [[see official Tibber support article](https://support.tibber.com/en/articles/4601431-tibber-referral-bonus#h_ae8df266c0)] 197 | 198 | Please consider [using my personal Tibber invitation link to join Tibber today](https://invite.tibber.com/6o0kqvzf) or Enter the following code: 6o0kqvzf (six, oscar, zero, kilo, quebec, victor, zulu, foxtrot) afterward in the Tibber App - TIA! 199 | 200 | --- 201 | 202 | [hacs]: https://hacs.xyz 203 | [hacsbadge]: https://img.shields.io/badge/HACS-Default-blue?style=for-the-badge&logo=homeassistantcommunitystore&logoColor=ccc 204 | 205 | [ghs]: https://github.com/sponsors/marq24 206 | [ghsbadge]: https://img.shields.io/github/sponsors/marq24?style=for-the-badge&logo=github&logoColor=ccc&link=https%3A%2F%2Fgithub.com%2Fsponsors%2Fmarq24&label=Sponsors 207 | 208 | [buymecoffee]: https://www.buymeacoffee.com/marquardt24 209 | [buymecoffeebadge]: https://img.shields.io/badge/buy%20me%20a-coffee-blue.svg?style=for-the-badge&logo=buymeacoffee&logoColor=ccc 210 | 211 | [paypal]: https://paypal.me/marq24 212 | [paypalbadge]: https://img.shields.io/badge/paypal-me-blue.svg?style=for-the-badge&logo=paypal&logoColor=ccc 213 | 214 | [hainstall]: https://my.home-assistant.io/redirect/config_flow_start/?domain=waterkotte_heatpump 215 | [hainstallbadge]: https://img.shields.io/badge/dynamic/json?style=for-the-badge&logo=home-assistant&logoColor=ccc&label=usage&suffix=%20installs&cacheSeconds=15600&url=https://analytics.home-assistant.io/custom_integrations.json&query=$.waterkotte_heatpump.total 216 | -------------------------------------------------------------------------------- /custom_components/waterkotte_heatpump/__init__.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import logging 3 | from datetime import timedelta 4 | from typing import List, Collection, Sequence, Any, Tuple 5 | 6 | from homeassistant.config_entries import ConfigEntry 7 | from homeassistant.const import CONF_ID, CONF_HOST, CONF_USERNAME, CONF_PASSWORD 8 | from homeassistant.core import HomeAssistant, Event, SupportsResponse 9 | from homeassistant.exceptions import ConfigEntryNotReady 10 | from homeassistant.helpers import config_validation as config_val, entity_registry as entity_reg 11 | from homeassistant.helpers.aiohttp_client import async_get_clientsession 12 | from homeassistant.helpers.entity import Entity, EntityDescription 13 | from homeassistant.helpers.typing import UNDEFINED, UndefinedType 14 | from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed 15 | 16 | from custom_components.waterkotte_heatpump.pywaterkotte_ha import WaterkotteClient 17 | from custom_components.waterkotte_heatpump.pywaterkotte_ha.const import ECOTOUCH 18 | from custom_components.waterkotte_heatpump.pywaterkotte_ha.error import TooManyUsersException, InvalidPasswordException 19 | from custom_components.waterkotte_heatpump.pywaterkotte_ha.tags import WKHPTag 20 | from . import service as waterkotte_service 21 | from .const import ( 22 | CONF_IP, 23 | CONF_POLLING_INTERVAL, 24 | CONF_TAGS_PER_REQUEST, 25 | CONF_BIOS, 26 | CONF_FW, 27 | CONF_SERIAL, 28 | CONF_SERIES, 29 | CONF_SYSTEMTYPE, 30 | CONF_ADD_SCHEDULE_ENTITIES, 31 | CONF_ADD_SERIAL_AS_ID, 32 | CONF_USE_VENT, 33 | CONF_USE_HEATING_CURVE, 34 | CONF_USE_DISINFECTION, 35 | NAME, 36 | DOMAIN, 37 | PLATFORMS, 38 | STARTUP_MESSAGE, 39 | SERVICE_SET_HOLIDAY, 40 | SERVICE_SET_SCHEDULE_DATA, 41 | SERVICE_SET_DISINFECTION_START_TIME, 42 | SERVICE_GET_ENERGY_BALANCE, 43 | SERVICE_GET_ENERGY_BALANCE_MONTHLY, 44 | FEATURE_VENT, 45 | FEATURE_HEATING_CURVE, 46 | FEATURE_DISINFECTION, 47 | FEATURE_CODE_GEN 48 | ) 49 | 50 | _LOGGER: logging.Logger = logging.getLogger(__package__) 51 | SCAN_INTERVAL = timedelta(seconds=60) 52 | CONFIG_SCHEMA = config_val.removed(DOMAIN, raise_if_present=False) 53 | 54 | 55 | async def async_setup(hass: HomeAssistant, config: dict): # pylint: disable=unused-argument 56 | """Set up this integration using YAML is not supported.""" 57 | return True 58 | 59 | 60 | async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry): 61 | if DOMAIN not in hass.data: 62 | value = "UNKOWN" 63 | _LOGGER.info(STARTUP_MESSAGE) 64 | hass.data.setdefault(DOMAIN, {"manifest_version": value}) 65 | 66 | coordinator = WKHPDataUpdateCoordinator(hass, config_entry) 67 | await coordinator.async_refresh() 68 | if not coordinator.last_update_success: 69 | raise ConfigEntryNotReady 70 | else: 71 | # here we can do some init stuff (like read all data)... 72 | pass 73 | 74 | hass.data[DOMAIN][config_entry.entry_id] = coordinator 75 | 76 | await hass.config_entries.async_forward_entry_setups(config_entry, PLATFORMS) 77 | 78 | service = waterkotte_service.WaterkotteHeatpumpService(hass, config_entry, coordinator) 79 | hass.services.async_register(DOMAIN, SERVICE_SET_HOLIDAY, service.set_holiday, 80 | supports_response=SupportsResponse.OPTIONAL) 81 | hass.services.async_register(DOMAIN, SERVICE_SET_SCHEDULE_DATA, service.set_schedule_data, 82 | supports_response=SupportsResponse.OPTIONAL) 83 | hass.services.async_register(DOMAIN, SERVICE_SET_DISINFECTION_START_TIME, service.set_disinfection_start_time, 84 | supports_response=SupportsResponse.OPTIONAL) 85 | hass.services.async_register(DOMAIN, SERVICE_GET_ENERGY_BALANCE, service.get_energy_balance, 86 | supports_response=SupportsResponse.ONLY) 87 | hass.services.async_register(DOMAIN, SERVICE_GET_ENERGY_BALANCE_MONTHLY, service.get_energy_balance_monthly, 88 | supports_response=SupportsResponse.ONLY) 89 | 90 | # we should check (in any CASE!) if the active tags might have... 91 | asyncio.create_task(coordinator.update_client_tag_list(hass, config_entry.data.get(CONF_ADD_SERIAL_AS_ID,False), config_entry.entry_id)) 92 | 93 | # ok we are done... 94 | config_entry.async_on_unload(config_entry.add_update_listener(entry_update_listener)) 95 | return True 96 | 97 | 98 | async def async_unload_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> bool: 99 | _LOGGER.debug(f"async_unload_entry() called for entry: {config_entry.entry_id}") 100 | unload_ok = await hass.config_entries.async_unload_platforms(config_entry, PLATFORMS) 101 | 102 | if unload_ok: 103 | if DOMAIN in hass.data and config_entry.entry_id in hass.data[DOMAIN]: 104 | # even if waterkotte does not support logout... I code it here... 105 | coordinator = hass.data[DOMAIN][config_entry.entry_id] 106 | await coordinator.bridge._internal_client.logout() 107 | 108 | hass.data[DOMAIN].pop(config_entry.entry_id) 109 | 110 | hass.services.async_remove(DOMAIN, SERVICE_SET_HOLIDAY) 111 | hass.services.async_remove(DOMAIN, SERVICE_SET_DISINFECTION_START_TIME) 112 | hass.services.async_remove(DOMAIN, SERVICE_GET_ENERGY_BALANCE) 113 | hass.services.async_remove(DOMAIN, SERVICE_GET_ENERGY_BALANCE_MONTHLY) 114 | 115 | return unload_ok 116 | 117 | 118 | async def entry_update_listener(hass: HomeAssistant, config_entry: ConfigEntry) -> None: 119 | _LOGGER.debug(f"entry_update_listener() called for entry: {config_entry.entry_id}") 120 | await hass.config_entries.async_reload(config_entry.entry_id) 121 | 122 | 123 | @staticmethod 124 | def generate_tag_list(hass: HomeAssistant, trim_unique_id:bool, config_entry_id: str) -> List[WKHPTag]: 125 | _LOGGER.info(f"(re)build tag list...") 126 | tags = [] 127 | if hass is not None: 128 | a_entity_reg = entity_reg.async_get(hass) 129 | if a_entity_reg is not None: 130 | # we query from the HA entity registry all entities that are created by this 131 | # 'config_entry' -> we use here just default api calls [no more hacks!] 132 | for entity in entity_reg.async_entries_for_config_entry(registry=a_entity_reg, 133 | config_entry_id=config_entry_id): 134 | if entity.disabled is False: 135 | a_temp_tag = (entity.unique_id) 136 | if trim_unique_id: 137 | a_temp_tag = a_temp_tag[0:a_temp_tag.rfind('_')] 138 | #_LOGGER.debug(f"found active entity: {entity.entity_id} using Tag: {a_temp_tag.upper()}") 139 | if a_temp_tag is not None and a_temp_tag.upper() in WKHPTag.__members__: 140 | if WKHPTag[a_temp_tag.upper()]: 141 | tags.append(WKHPTag[a_temp_tag.upper()]) 142 | else: 143 | _LOGGER.warning(f"Tag: {a_temp_tag} not found in WKHPTag.__members__ !") 144 | return tags 145 | 146 | 147 | class WKHPDataUpdateCoordinator(DataUpdateCoordinator): 148 | def __init__(self, hass: HomeAssistant, config_entry): 149 | self.name = config_entry.title 150 | self.is_multi_instances = config_entry.data.get(CONF_ADD_SERIAL_AS_ID, False) 151 | if self.is_multi_instances: 152 | self.serial_id_addon = config_entry.data.get(CONF_SERIAL, "") 153 | 154 | self._config_entry = config_entry 155 | self.add_schedule_entities = config_entry.options.get(CONF_ADD_SCHEDULE_ENTITIES, 156 | config_entry.data.get(CONF_ADD_SCHEDULE_ENTITIES, False)) 157 | self.available_features = [] 158 | if CONF_USE_VENT in config_entry.data and config_entry.data[CONF_USE_VENT]: 159 | self.available_features.append(FEATURE_VENT) 160 | if CONF_USE_HEATING_CURVE in config_entry.data and config_entry.data[CONF_USE_HEATING_CURVE]: 161 | self.available_features.append(FEATURE_HEATING_CURVE) 162 | if CONF_USE_DISINFECTION in config_entry.data and config_entry.data[CONF_USE_DISINFECTION]: 163 | self.available_features.append(FEATURE_DISINFECTION) 164 | _LOGGER.debug(f"available_features: {self.available_features}") 165 | 166 | _host = config_entry.options.get(CONF_HOST, config_entry.data.get(CONF_HOST)) 167 | _user = config_entry.options.get(CONF_USERNAME, config_entry.data.get(CONF_USERNAME, "waterkotte")) 168 | _pwd = config_entry.options.get(CONF_PASSWORD, config_entry.data.get(CONF_PASSWORD, "waterkotte")) 169 | _system_type = config_entry.options.get(CONF_SYSTEMTYPE, config_entry.data.get(CONF_SYSTEMTYPE, ECOTOUCH)) 170 | _tags_num = config_entry.options.get(CONF_TAGS_PER_REQUEST, config_entry.data.get(CONF_TAGS_PER_REQUEST, 10)) 171 | _tags = generate_tag_list(hass=hass, trim_unique_id=self.is_multi_instances, config_entry_id=config_entry.entry_id) 172 | 173 | self.bridge = WaterkotteClient(host=_host, username=_user, pwd=_pwd, system_type=_system_type, 174 | web_session=async_get_clientsession(hass), tags=_tags, 175 | tags_per_request=_tags_num, lang=hass.config.language.lower()) 176 | 177 | global SCAN_INTERVAL 178 | # update_interval can be adjusted in the options (not for WebAPI) 179 | SCAN_INTERVAL = timedelta(seconds=config_entry.options.get(CONF_POLLING_INTERVAL, 180 | config_entry.data.get(CONF_POLLING_INTERVAL, 60))) 181 | 182 | fw = config_entry.options.get(CONF_IP, config_entry.data.get(CONF_IP)) 183 | bios = config_entry.options.get(CONF_BIOS, config_entry.data.get(CONF_BIOS)) 184 | self._device_info_dict = { 185 | "identifiers": { 186 | ("DOMAIN", DOMAIN), 187 | ("IP", config_entry.options.get(CONF_IP, config_entry.data.get(CONF_IP))), 188 | }, 189 | "manufacturer": NAME, 190 | "name": NAME, 191 | "model": config_entry.options.get(CONF_SERIES, config_entry.data.get(CONF_SERIES)), 192 | "sw_version": f"{fw} BIOS: {bios}", 193 | "hw_version": config_entry.options.get(CONF_ID, config_entry.data.get(CONF_ID)) 194 | } 195 | 196 | super().__init__(hass, _LOGGER, name=DOMAIN, update_interval=SCAN_INTERVAL) 197 | 198 | async def update_client_tag_list(self, hass: HomeAssistant, trim_unique_id: bool, entry_id: str): 199 | _LOGGER.debug(f"rechecking active tags... in 15sec") 200 | await asyncio.sleep(15) 201 | 202 | _LOGGER.debug(f"rechecking active tags NOW!") 203 | self.bridge.tags = generate_tag_list(hass, trim_unique_id, entry_id) 204 | 205 | _LOGGER.debug(f"active tags checked... now refresh sensor data") 206 | await self.async_refresh() 207 | 208 | # Callable[[Event], Any] 209 | def __call__(self, evt: Event) -> bool: 210 | _LOGGER.debug(f"Event arrived: {evt}") 211 | return True 212 | 213 | async def _async_update_data(self): 214 | """Update data via library.""" 215 | try: 216 | await self.bridge.login() 217 | _LOGGER.info(f"number of entities to query: {len(self.bridge.tags)} (1 entity can consist of n-tags)") 218 | result = await self.bridge.async_get_data() 219 | _LOGGER.info(f"number of entity values read: {len(result)}") 220 | 221 | if self.data is None: 222 | self.data = {} 223 | 224 | for a_tag_in_result in result: 225 | if result[a_tag_in_result]["status"] == "S_OK": 226 | self.data[a_tag_in_result] = result[a_tag_in_result] 227 | 228 | return self.data 229 | 230 | except UpdateFailed as exception: 231 | raise UpdateFailed() from exception 232 | except InvalidPasswordException as invalid_pwd: 233 | _LOGGER.info(f"invalid password for waterkotte! {invalid_pwd}") 234 | raise UpdateFailed() from invalid_pwd 235 | except TooManyUsersException as too_many_users: 236 | _LOGGER.info(f"TooManyUsers response from waterkotte - waiting 30sec and then retry...") 237 | await asyncio.sleep(30) 238 | raise UpdateFailed() from too_many_users 239 | except Exception as other: 240 | _LOGGER.error(f"unexpected: {other}") 241 | raise UpdateFailed() from other 242 | 243 | async def async_read_values(self, tags: Sequence[WKHPTag]) -> dict: 244 | """Get data from the API.""" 245 | ret = await self.bridge.async_read_values(tags) 246 | return ret 247 | 248 | async def async_write_tags(self, kv_pairs: Collection[Tuple[WKHPTag, Any]]) -> dict: 249 | """Get data from the API.""" 250 | ret = await self.bridge.async_write_values(kv_pairs) 251 | return ret 252 | 253 | async def async_write_tag(self, tag: WKHPTag, value, entity: Entity = None): 254 | """Update single data""" 255 | result = await self.bridge.async_write_value(tag, value) 256 | _LOGGER.debug(f"write result: {result}") 257 | 258 | if tag in result: 259 | self.data[tag] = result[tag] 260 | else: 261 | _LOGGER.error(f"could not write value: '{value}' to: {tag} result was: {result}") 262 | 263 | # after we have written something to the Waterkotte we should force an update of the data... 264 | if entity is not None: 265 | entity.async_schedule_update_ha_state(force_refresh=True) 266 | 267 | 268 | class WKHPBaseEntity(Entity): 269 | _attr_should_poll = False 270 | _attr_has_entity_name = True 271 | 272 | def __init__(self, coordinator: WKHPDataUpdateCoordinator, description: EntityDescription) -> None: 273 | if description.feature is not None and FEATURE_CODE_GEN == description.feature: 274 | self.code_generated = True 275 | else: 276 | self.code_generated = False 277 | self._attr_translation_key = description.key.lower() 278 | self.coordinator = coordinator 279 | self.entity_description = description 280 | 281 | # check, if the feature should be enabled by default (if activated during setup) 282 | if not description.entity_registry_enabled_default and description.feature is not None: 283 | if description.feature in self.coordinator.available_features: 284 | self._attr_entity_registry_enabled_default = True 285 | 286 | if self.coordinator.is_multi_instances: 287 | self.entity_id = f"{DOMAIN}.wkh_{self.coordinator.serial_id_addon}_{self._attr_translation_key}" 288 | else: 289 | self.entity_id = f"{DOMAIN}.wkh_{self._attr_translation_key}" 290 | 291 | def _name_internal(self, device_class_name: str | None, 292 | platform_translations: dict[str, Any], ) -> str | UndefinedType | None: 293 | if self.code_generated: 294 | return self._name_internal_code_generated(self._attr_translation_key, platform_translations) 295 | else: 296 | return super()._name_internal(device_class_name, platform_translations) 297 | 298 | def _name_internal_code_generated(self, key, platform_translations: dict[str, Any]): 299 | temp = key.lower().replace('_', ' ') 300 | temp = temp.replace(' enable', '') 301 | temp = temp.replace(' value', '') 302 | a_list = ["schedule", "heating", "cooling", "water", "pool", "solar", "pv", 303 | "mix1", "mix2", "mix3", "buffer tank circulation pump", "adjust", 304 | "1mo", "2tu", "3we", "4th", "5fr", "6sa", "7su", 305 | "start time", "end time"] 306 | for a_key in a_list: 307 | f_key = f"component.{self.platform.platform_name}.entity.code_gen.{a_key.replace(' ', '_')}.name" 308 | if f_key in platform_translations: 309 | temp = temp.replace(a_key, platform_translations.get(f_key)) 310 | else: 311 | _LOGGER.warning(f"{a_key} -> {f_key} not found in platform_translations") 312 | 313 | return temp # .title() 314 | 315 | @property 316 | def wkhp_tag(self): 317 | """Return a unique ID to use for this entity.""" 318 | return self.entity_description.tag 319 | 320 | @property 321 | def device_info(self) -> dict: 322 | return self.coordinator._device_info_dict 323 | 324 | @property 325 | def available(self): 326 | """Return True if entity is available.""" 327 | return self.coordinator.last_update_success 328 | 329 | @property 330 | def unique_id(self): 331 | """Return a unique ID to use for this entity.""" 332 | # sensor_key = self.entity_description.key 333 | # device_key = self.coordinator.config_entry.data[CONF_SERIAL] 334 | # return f"{device_key}_{sensor}" 335 | if self.coordinator.is_multi_instances: 336 | return f"{self.entity_description.key}_{self.coordinator.serial_id_addon}" 337 | else: 338 | return self.entity_description.key 339 | 340 | async def async_added_to_hass(self): 341 | """Connect to dispatcher listening for entity data notifications.""" 342 | self.async_on_remove(self.coordinator.async_add_listener(self.async_write_ha_state)) 343 | 344 | async def async_update(self): 345 | """Update entity.""" 346 | await self.coordinator.async_request_refresh() 347 | 348 | @property 349 | def should_poll(self) -> bool: 350 | """Entities do not individually poll.""" 351 | return False 352 | 353 | def _friendly_name_internal(self) -> str | None: 354 | """Return the friendly name. 355 | 356 | If has_entity_name is False, this returns self.name 357 | If has_entity_name is True, this returns device.name + self.name 358 | """ 359 | name = self.name 360 | if name is UNDEFINED: 361 | name = None 362 | 363 | if not self.has_entity_name or not (device_entry := self.device_entry): 364 | return name 365 | 366 | device_name = device_entry.name_by_user or device_entry.name 367 | if name is None and self.use_device_name: 368 | return device_name 369 | 370 | # we overwrite the default impl here and just return our 'name' 371 | # return f"{device_name} {name}" if device_name else name 372 | if device_entry.name_by_user is not None: 373 | return f"{device_entry.name_by_user} {name}" if device_name else name 374 | else: 375 | return f"[WKHP] {name}" 376 | -------------------------------------------------------------------------------- /custom_components/waterkotte_heatpump/translations/en.json: -------------------------------------------------------------------------------- 1 | { 2 | "selector": { 3 | "system_type": { 4 | "options": { 5 | "ecotouch": "EcoTouch Mode [web-form interface require username & password]", 6 | "easycon": "EasyCon Mode [older Waterkotte Models with basic-auth login]" 7 | } 8 | }, 9 | "set_schedule_data_schedule_type": { 10 | "options": { 11 | "heating": "Heating", 12 | "cooling": "Cooling", 13 | "water": "Hot Water", 14 | "pool": "Pool", 15 | "mix1": "Mixer 1", 16 | "mix2": "Mixer 2", 17 | "mix3": "Mixer 3", 18 | "buffer_tank_circulation_pump": "Circulation pump buffer tank", 19 | "solar": "Solar Control", 20 | "pv": "Photovoltaic" 21 | } 22 | }, 23 | "set_schedule_data_schedule_days": { 24 | "options": { 25 | "1mo": "Mondays", 26 | "2tu": "Tuesdays", 27 | "3we": "Wednesdays", 28 | "4th": "Thursdays", 29 | "5fr": "Fridays", 30 | "6sa": "Saturdays", 31 | "7su": "Sundays" 32 | } 33 | } 34 | }, 35 | "config": { 36 | "step": { 37 | "user": { 38 | "description": "If you need help with the configuration have a look here: https://github.com/marq24/ha-waterkotte.", 39 | "data": { 40 | "system_type": "Watterkotte Interface-Typ" 41 | }, 42 | "data_description": { 43 | "system_type": "If you need to provide a username & password to log in to the Waterkotte web interface, please select the interface type: EcoTouch Mode, else the EasyCon Mode." 44 | } 45 | }, 46 | "user_ecotouch": { 47 | "description": "Important note: With the 'Adjust Schedules Service' you can adjust all schedules of your Waterkotte heat pump. Adding the optional entities is not necessary for this. These are only needed if you want to modify or display the schedule settings via your dashboard or one of your automations.\r\rIf you are unsure, do not add the optional scheduling entities in the first step. They can be added at any time via a configuration settings at a later date.", 48 | "data": { 49 | "host": "Host or IP", 50 | "username": "Username", 51 | "password": "Password", 52 | "polling_interval": "Polling Interval in seconds", 53 | "tags_per_request": "Number of tags to fetch in a single request (max. 75)", 54 | "add_schedule_entities": "Add the optional Schedule-Entities (650+)", 55 | "add_serial_as_id": "Do you want to configure multiple Waterkotte Systems in your HA installation?" 56 | } 57 | }, 58 | "user_easycon": { 59 | "description": "If you need help with the configuration have a look here: https://github.com/marq24/ha-waterkotte.", 60 | "data": { 61 | "host": "Host or IP", 62 | "polling_interval": "Polling Interval in seconds", 63 | "tags_per_request": "Number of tags to fetch in a single request (max. 75)", 64 | "add_serial_as_id": "Do you want to configure multiple Waterkotte Systems in your HA installation?" 65 | } 66 | }, 67 | "features": { 68 | "description": "Activation of additional Sensors und Controls:\n(These can also be activated later manually)", 69 | "data": { 70 | "use_vent": "for an 'air source heat pump outdoor unit'", 71 | "use_heating_curve": "to adjust 'heating curves'", 72 | "use_disinfection": "to adjust the 'Water disinfection'", 73 | "use_pool": "for a 'pool' unit' (incl. heating curve)" 74 | } 75 | } 76 | }, 77 | "error": { 78 | "auth": "Host/IP or the Password is wrong - could not reach system", 79 | "type": "No connection to Waterkotte possible - are you sure that you have selected previously the correct Interface-Type?" 80 | }, 81 | "abort": { 82 | "single_instance_allowed": "Only a single instance is allowed." 83 | } 84 | }, 85 | "options": { 86 | "step": { 87 | "user": { 88 | "description": "If you need help with the configuration have a look here: https://github.com/marq24/ha-waterkotte.\r\rImportant note: With the 'Adjust Schedules Service' you can adjust all schedules of your Waterkotte heating. Adding the optional entities is not necessary for this. These are only needed if you want to modify or display the schedule settings via your dashboard or one of your automations.", 89 | "data": { 90 | "username": "Username", 91 | "password": "Password", 92 | "polling_interval": "Polling Interval in seconds", 93 | "tags_per_request": "Number of tags to fetch in a single request (max. 75)", 94 | "add_schedule_entities": "Add the optional Schedule-Entities (650+)" 95 | } 96 | } 97 | } 98 | }, 99 | "entity": { 100 | "binary_sensor": { 101 | "state_sourcepump": {"name": "Source pump"}, 102 | "state_heatingpump": {"name": "Heat pump"}, 103 | "state_evd": {"name": "EVD"}, 104 | "state_compressor": {"name": "Compressor"}, 105 | "state_compressor2": {"name": "Compressor II"}, 106 | "state_external_heater": {"name": "Electrical heater"}, 107 | "state_alarm": {"name": "Notifications"}, 108 | "state_cooling": {"name": "Cooling"}, 109 | "state_water": {"name": "Hot water"}, 110 | "state_pool": {"name": "Pool"}, 111 | "state_solar": {"name": "Solar"}, 112 | "state_cooling4way": {"name": "4-way valve"}, 113 | "status_heating": {"name": "Heating mode"}, 114 | "status_water": {"name": "Hot water mode"}, 115 | "status_cooling": {"name": "Cooling mode"}, 116 | "status_pool": {"name": "Pool mode"}, 117 | "status_solar": {"name": "Solar mode"}, 118 | "status_heating_circulation_pump": {"name": "Circulation pump heating mode"}, 119 | "status_solar_circulation_pump": {"name": "Circulation pump solar mode"}, 120 | "status_buffer_tank_circulation_pump": {"name": "Circulation pump buffer tank mode"}, 121 | "status_compressor": {"name": "Compressor mode"}, 122 | "state_blocking_time": {"name": "Blocking time"}, 123 | "state_test_run": {"name": "Test run"}, 124 | "state_heating_circulation_pump_d425": {"name": "Circulation pump heating"}, 125 | "state_buffertank_circulation_pump_d377": {"name": "Circulation pump buffer tank"}, 126 | "state_pool_circulation_pump_d549": {"name": "Circulation pump pool"}, 127 | "state_mix1_circulation_pump_d248": {"name": "Circulation pump mixer 1"}, 128 | "state_mix2_circulation_pump_d291": {"name": "Circulation pump mixer 2"}, 129 | "state_mix3_circulation_pump_d334": {"name": "Circulation pump mixer 3"}, 130 | "state_mix1_circulation_pump_d563": {"name": "Circulation pump mixer 1 [D563]"}, 131 | "basicvent_status_bypass_active_d1432": {"name": "Vent Bypass Active"}, 132 | "basicvent_status_humidifier_active_d1433": {"name": "Vent Humidifier Active"}, 133 | "basicvent_status_comfort_bypass_active_d1465": {"name": "Vent Comfort Bypass Active"}, 134 | "basicvent_status_smart_bypass_active_d1466": {"name": "Vent Smart Bypass Active"}, 135 | "basicvent_status_holiday_enabled_d1503": {"name": "Vent Holiday Enabled"}, 136 | "basicvent_filter_change_display_d1469": {"name": "Vent Filter change required"} 137 | }, 138 | "number": { 139 | "temperature_return_setpoint": {"name": "T setpoint"}, 140 | "temperature_cooling_setpoint": {"name": "T Cooling"}, 141 | "temperature_cooling_outdoor_limit": {"name": "T out begin"}, 142 | "temperature_cooling_flow_limit": {"name": "Flow temperature limitation"}, 143 | "temperature_heating_setpoint": {"name": "Heating temperature"}, 144 | "temperature_heating_adjust": {"name": "Temperature adjustment"}, 145 | "temperature_heating_hysteresis": {"name": "Hysteresis setpoint"}, 146 | "temperature_mix1_adjust": {"name": "Temperature adjustment"}, 147 | "temperature_mix2_adjust": {"name": "Temperature adjustment"}, 148 | "temperature_mix3_adjust": {"name": "Temperature adjustment"}, 149 | "temperature_pool_adjust": {"name": "Temperature adjustment"}, 150 | "temperature_heating_hc_limit": {"name": "T heating limit"}, 151 | "temperature_heating_hc_target": {"name": "T heating limit target"}, 152 | "temperature_heating_hc_outdoor_norm": {"name": "T norm outdoor"}, 153 | "temperature_heating_hc_norm": {"name": "T norm heating circle"}, 154 | "temperature_heating_setpointlimit_max": {"name": "Limit for setpoint (Max.)"}, 155 | "temperature_heating_setpointlimit_min": {"name": "Limit for setpoint (Min.)"}, 156 | "temperature_heating_powlimit_min": {"name": "Min. heating output"}, 157 | "temperature_heating_powlimit_max": {"name": "Max. heating output"}, 158 | "temperature_water_setpoint": {"name": "Demanded temperature"}, 159 | "temperature_water_hysteresis": {"name": "Hysteresis setpoint"}, 160 | "temperature_water_powlimit_min": {"name": "Min. hot water output"}, 161 | "temperature_water_powlimit_max": {"name": "Max. hot water output"}, 162 | "temperature_mix1_hc_limit": {"name": "T heating limit"}, 163 | "temperature_mix1_hc_target": {"name": "T heating limit target"}, 164 | "temperature_mix1_hc_outdoor_norm": {"name": "T norm outdoor"}, 165 | "temperature_mix1_hc_heating_norm": {"name": "T norm heating circle"}, 166 | "temperature_mix1_hc_max": {"name": "Max. temperature in mixing circle"}, 167 | "temperature_mix2_hc_limit": {"name": "T heating limit"}, 168 | "temperature_mix2_hc_target": {"name": "T heating limit target"}, 169 | "temperature_mix2_hc_outdoor_norm": {"name": "T norm outdoor"}, 170 | "temperature_mix2_hc_heating_norm": {"name": "T norm heating circle"}, 171 | "temperature_mix2_hc_max": {"name": "Max. temperature in mixing circle"}, 172 | "temperature_mix3_hc_limit": {"name": "T heating limit"}, 173 | "temperature_mix3_hc_target": {"name": "T heating limit target"}, 174 | "temperature_mix3_hc_outdoor_norm": {"name": "T norm outdoor"}, 175 | "temperature_mix3_hc_heating_norm": {"name": "T norm heating circle"}, 176 | "temperature_mix3_hc_max": {"name": "Max. temperature in mixing circle"}, 177 | "temperature_water_disinfection": {"name": "Demanded temperature"}, 178 | "schedule_water_disinfection_duration": {"name": "Max. runtime"}, 179 | "temperature_pool_setpoint": {"name": "Demanded temperature"}, 180 | "temperature_pool_hysteresis": {"name": "Hysteresis setpoint"}, 181 | "temperature_pool_hc_limit": {"name": "T heating limit"}, 182 | "temperature_pool_hc_target": {"name": "T heating limit target"}, 183 | "temperature_pool_hc_outdoor_norm": {"name": "T norm outdoor"}, 184 | "temperature_pool_hc_norm": {"name": "T norm heating circle"}, 185 | "temperature_pool_max_runtime": {"name": "Max. pool heating runtime"}, 186 | "temperature_pool_setpointlimit": {"name": "Limit for pool setpoint"}, 187 | "temperature_pool_powlimit_min": {"name": "Min. pool output"}, 188 | "temperature_pool_powlimit_max": {"name": "Max. pool output"}, 189 | "basicvent_incoming_fan_manual_speed_percent": {"name": "Vent 1 (outer) manual Speed"}, 190 | "basicvent_outgoing_fan_manual_speed_percent": {"name": "Vent 2 (inner) manual Speed"}, 191 | "pumpservice_sourcepump_pre_runtime_i1278": {"name": "Service: Sourcepump pre runtime"}, 192 | "pumpservice_sourcepump_post_runtime_i1279": {"name": "Service: Sourcepump post runtime"}, 193 | "pumpservice_sourcepump_anti_jamming_i1280": {"name": "Service: Sourcepump anti jamming"}, 194 | "pumpservice_sourcepump_temp_on_lower_a1539": {"name": "Service: Sourcepump Temp. Source ON <"}, 195 | "pumpservice_sourcepump_heatmode_minspeed_a485": {"name": "Service: Sourcepump Heating min speeed"}, 196 | "pumpservice_sourcepump_heatmode_maxspeed_a486": {"name": "Service: Sourcepump Heating max speed"}, 197 | "pumpservice_sourcepump_heatmode_source_temperature_a479": {"name": "Service: Sourcepump Heating ΔT heat source"}, 198 | "pumpservice_sourcepump_coolingmode_minspeed_a1032": {"name": "Service: Sourcepump Cooling min speed"}, 199 | "pumpservice_sourcepump_coolingmode_maxspeed_a1033": {"name": "Service: Sourcepump Cooling max speed"}, 200 | "pumpservice_sourcepump_coolingmode_source_temperature_a1034": {"name": "Service: Sourcepump Cooling Source Temperature"}, 201 | "temperature_room_target_a100": {"name": "T Room target"} 202 | }, 203 | "select": { 204 | "temperature_heating_mode": { 205 | "name": "Heating Control", 206 | "state": { 207 | "mode0": "Weather-compensated", 208 | "mode1": "Manual Setpoint", 209 | "mode2": "Setpoint BMS", 210 | "mode3": "Setpoint EXT", 211 | "mode4": "Setpoint 0-10V", 212 | "mode5": "Based on Mixing circle" 213 | } 214 | }, 215 | "temperature_pool_mode": { 216 | "name": "Pool Control", 217 | "state": { 218 | "mode0": "Weather-compensated", 219 | "mode1": "Setpoint", 220 | "mode2": "Setpoint BMS", 221 | "mode3": "Setpoint EXT" 222 | } 223 | }, 224 | "basicvent_operation_mode_i4582": { 225 | "name": "Vent Operation mode (I4582)", 226 | "state": { 227 | "mode0": "Day", 228 | "mode1": "Night", 229 | "mode2": "Scheduled", 230 | "mode3": "Party", 231 | "mode4": "Holiday", 232 | "mode5": "Bypass" 233 | } 234 | }, 235 | "basicvent_operation_mode_alt": { 236 | "name": "Vent Operation mode (alternative)", 237 | "state": { 238 | "mode0": "Day", 239 | "mode1": "Night", 240 | "mode2": "Scheduled", 241 | "mode3": "Party", 242 | "mode4": "Holiday", 243 | "mode5": "Bypass" 244 | } 245 | }, 246 | "enable_cooling": {"name": "Operation mode cooling"}, 247 | "enable_heating": {"name": "Operation mode heating"}, 248 | "enable_pv": {"name": "Operation mode PV"}, 249 | "enable_warmwater": {"name": "Operation mode hot water"}, 250 | "enable_pool": {"name": "Operation mode pool"}, 251 | "enable_external_heater": {"name": "Operation mode external heater"}, 252 | "enable_mixing1": {"name": "Operation mode Mixer 1"}, 253 | "enable_mixing2": {"name": "Operation mode Mixer 2"}, 254 | "enable_mixing3": {"name": "Operation mode Mixer 3"}, 255 | "pumpservice_sourcepump_i1281": { 256 | "name": "Service: Sourcepump", 257 | "state": { 258 | "0": "Off", 259 | "1": "On", 260 | "2": "Auto" 261 | } 262 | }, 263 | "pumpservice_sourcepump_mode_i1764": { 264 | "name": "Service: Sourcepump Mode", 265 | "state": { 266 | "0": "0-10V", 267 | "1": "PWM T12", 268 | "2": "PWM T13" 269 | } 270 | }, 271 | "pumpservice_sourcepump_heatmode_regulation_by_i1752": { 272 | "name": "Service: Sourcepump Heating regulation by…", 273 | "state": { 274 | "0": "Spreading", 275 | "1": "Temperature" 276 | } 277 | }, 278 | "pumpservice_sourcepump_heatmode_control_behaviour_d789": { 279 | "name": "Service: Sourcepump Heating control behaviour", 280 | "state": { 281 | "0": "Standard", 282 | "1": "Inverted" 283 | } 284 | }, 285 | "pumpservice_sourcepump_heatmode_regulation_start_d996": { 286 | "name": "Service: Sourcepump Heating regulation start with…", 287 | "state": { 288 | "0": "Min. Speed", 289 | "1": "Max. Speed" 290 | } 291 | }, 292 | "pumpservice_sourcepump_coolingmode_regulation_by_i2102": { 293 | "name": "Service: Sourcepump Cooling regulation by…", 294 | "state": { 295 | "0": "Spreading", 296 | "1": "Temperature", 297 | "2": "Temperature Sekundär" 298 | } 299 | }, 300 | "pumpservice_sourcepump_coolingmode_control_behaviour_d995": { 301 | "name": "Service: Sourcepump Cooling control behaviour", 302 | "state": { 303 | "0": "Standard", 304 | "1": "Inverted" 305 | } 306 | }, 307 | "pumpservice_sourcepump_coolingmode_regulation_start_d997": { 308 | "name": "Service: Sourcepump Cooling regulation start with…", 309 | "state": { 310 | "0": "Min. Speed", 311 | "1": "Max. Speed" 312 | } 313 | }, 314 | "room_influence_a101_or_i264": { 315 | "name": "Room Influence", 316 | "state": { 317 | "0": "0%", 318 | "1": "50%", 319 | "2": "100%", 320 | "3": "150%", 321 | "4": "200%" 322 | } 323 | } 324 | }, 325 | "sensor": { 326 | "energy_consumption_total_year": {"name": "Electrical year performance"}, 327 | "compressor_electric_consumption_year": {"name": "Compressor year performance"}, 328 | "sourcepump_electric_consumption_year": {"name": "Heat source pump year performance"}, 329 | "electrical_heater_electric_consumption_year": {"name": "Electrical heater year performance"}, 330 | "energy_production_total_year": {"name": "Thermal year performance"}, 331 | "heating_energy_production_year": {"name": "Heating year performance"}, 332 | "hot_water_energy_production_year": {"name": "Hot water year performance"}, 333 | "pool_energy_production_year": {"name": "Pool year performance"}, 334 | "cooling_energy_year": {"name": "Cooling year performance"}, 335 | "temperature_outside": {"name": "Outdoor temperature"}, 336 | "temperature_outside_1h": {"name": "Outdoor temperature 1h"}, 337 | "temperature_outside_24h": {"name": "Outdoor temperature 24h"}, 338 | "temperature_source_entry": {"name": "T source entry"}, 339 | "temperature_source_exit": {"name": "T source exit"}, 340 | "temperature_evaporation": {"name": "T evaporation"}, 341 | "temperature_suction_line": {"name": "T suction line"}, 342 | "temperature_return": {"name": "T return"}, 343 | "temperature_flow": {"name": "T flow"}, 344 | "temperature_condensation": {"name": "T condensation"}, 345 | "temperature_buffertank": {"name": "Temperature buffer tank"}, 346 | "temperature_room": {"name": "Room temperature"}, 347 | "temperature_room_1h": {"name": "Room temperature 1h"}, 348 | "temperature_heating": {"name": "Actual temperature"}, 349 | "temperature_heating_demand": {"name": "Demanded temperature"}, 350 | "temperature_cooling": {"name": "Actual temperature"}, 351 | "temperature_cooling_demand": {"name": "Actual temperature"}, 352 | "temperature_water": {"name": "Hot water temperature"}, 353 | "temperature_water_demand": {"name": "Demanded temperature"}, 354 | "temperature_mix1": {"name": "Actual temperature"}, 355 | "temperature_mix1_percent": {"name": "Y"}, 356 | "temperature_mix1_demand": {"name": "Demanded temperature"}, 357 | "temperature_mix2": {"name": "Actual temperature"}, 358 | "temperature_mix2_percent": {"name": "Y"}, 359 | "temperature_mix2_demand": {"name": "Demanded temperature"}, 360 | "temperature_mix3": {"name": "Actual temperature"}, 361 | "temperature_mix3_percent": {"name": "Y"}, 362 | "temperature_mix3_demand": {"name": "Demanded temperature"}, 363 | "temperature_pool": {"name": "Actual temperature"}, 364 | "temperature_pool_demand": {"name": "Demanded temperature"}, 365 | "temperature_solar": {"name": "T Solar"}, 366 | "temperature_solar_exit": {"name": "Exit temperature solar collector"}, 367 | "temperature_discharge": {"name": "Discharge temperature"}, 368 | "pressure_evaporation": {"name": "p evaporation"}, 369 | "pressure_condensation": {"name": "p condensation"}, 370 | "pressure_water": {"name": "Water pressure"}, 371 | "position_expansion_valve": {"name": "Valve opening EEV"}, 372 | "suction_gas_overheating": {"name": "suction gas overheating"}, 373 | "power_electric": {"name": "Electrical power"}, 374 | "power_heating": {"name": "Thermal power"}, 375 | "power_cooling": {"name": "Cooling power"}, 376 | "cop_heating": {"name": "COP"}, 377 | "cop_cooling": {"name": "COP cooling power"}, 378 | "percent_heat_circ_pump": {"name": "Speed heating pump"}, 379 | "percent_source_pump": {"name": "Speed source pump"}, 380 | "percent_compressor": {"name": "Power compressor"}, 381 | "waterkotte_bios_time": {"name": "BIOS Time"}, 382 | "holiday_start_time": {"name": "Holiday start"}, 383 | "holiday_end_time": {"name": "Holiday end"}, 384 | "schedule_water_disinfection_start_time": {"name": "Start time"}, 385 | "state_service": {"name": "Service data"}, 386 | "alarm_bits": {"name": "Malfunctions"}, 387 | "interruption_bits": {"name": "Interruptions"}, 388 | "basicvent_filter_change_operating_days_a4498": {"name": "Vent Air-Filter-Change operating hours"}, 389 | "basicvent_filter_change_remaining_operating_days_a4504": {"name": "Vent Air-Filter-Change operating hours remaining time"}, 390 | "basicvent_humidity_value_a4990": {"name": "Vent humidity"}, 391 | "basicvent_co2_value_a4992": {"name": "Vent CO2 concentration"}, 392 | "basicvent_voc_value_a4522": {"name": "Vent VOC"}, 393 | "basicvent_incoming_fan_rpm_a4551": {"name": "Vent 1 (outer) rotation"}, 394 | "basicvent_incoming_fan_a4986": {"name": "Vent 1 (outer) power"}, 395 | "basicvent_temperature_incoming_air_before_oda_a5000": {"name": "Vent Outdoor air (ODA) temperatur"}, 396 | "basicvent_temperature_incoming_air_after_sup_a4996": {"name": "Vent Supply air (SUP) temperatur"}, 397 | "basicvent_outgoing_fan_rpm_a4547": {"name": "Vent 2 (inner) rotation"}, 398 | "basicvent_outgoing_fan_a4984": {"name": "Vent 2 (inner) power"}, 399 | "basicvent_temperature_outgoing_air_before_eth_a4998": {"name": "Vent Extract air (ETH) temperature"}, 400 | "basicvent_temperature_outgoing_air_after_eeh_a4994": {"name": "Vent Exhaust air (EEH) temperature"}, 401 | "basicvent_energy_save_total_a4387": {"name": "Vent energy savings total"}, 402 | "basicvent_energy_save_current_a4389": {"name": "Vent energy saving now"}, 403 | "basicvent_energy_recovery_rate_a4391": {"name": "Vent Heat recovery rate"}, 404 | "operating_hours_compressor_1": {"name": "Operating hours Compressor"}, 405 | "operating_hours_compressor_2": {"name": "Operating hours Compressor II"}, 406 | "operating_hours_circulation_pump": {"name": "Operating hours Circulation pump"}, 407 | "operating_hours_source_pump": {"name": "Operating hours Source pump"}, 408 | "operating_hours_solar": {"name": "Operating hours solar"}, 409 | "temperature_room_1h_a98": {"name": "Room temperature 1h (alt)"} 410 | }, 411 | "switch": { 412 | "holiday_enabled": {"name": "Holiday"}, 413 | "schedule_water_disinfection_1mo": {"name": "Monday"}, 414 | "schedule_water_disinfection_2tu": {"name": "Tuesday"}, 415 | "schedule_water_disinfection_3we": {"name": "Wednesday"}, 416 | "schedule_water_disinfection_4th": {"name": "Thursday"}, 417 | "schedule_water_disinfection_5fr": {"name": "Friday"}, 418 | "schedule_water_disinfection_6sa": {"name": "Saturday"}, 419 | "schedule_water_disinfection_7su": {"name": "Sunday"}, 420 | "permanent_heating_circulation_pump_winter_d1103": {"name": "Continuous operation heating pump during heating period"}, 421 | "permanent_heating_circulation_pump_summer_d1104": {"name": "Continuous operation heating pump during cooling period"}, 422 | "basicvent_filter_change_operating_hours_reset_d1544": {"name": "Vent Air-Filter-Change operating hours RESET"}, 423 | "basicvent_incoming_fan_manual_mode": {"name": "Vent 1 (outer) Manual-Mode"}, 424 | "basicvent_outgoing_fan_manual_mode": {"name": "Vent 2 (inner) Manual-Mode"}, 425 | "pumpservice_sourcepump_cable_break_monitoring_d881": {"name": "Service: Sourcepump cable-break Monitor"}, 426 | "pumpservice_sourcepump_regeneration_d1294": {"name": "Service: Sourcepump regeneration"} 427 | }, 428 | "code_gen": { 429 | "schedule": {"name": "Schedule"}, 430 | "heating": {"name": "Heating"}, 431 | "cooling": {"name": "Cooling"}, 432 | "water": {"name": "Hot Water"}, 433 | "pool": {"name": "Pool"}, 434 | "mix1": {"name": "Mixer 1"}, 435 | "mix2": {"name": "Mixer 2"}, 436 | "mix3": {"name": "Mixer 3"}, 437 | "buffer_tank_circulation_pump": {"name": "Circulation pump buffer tank"}, 438 | "solar": {"name": "Solar Control"}, 439 | "pv": {"name": "Photovoltaic"}, 440 | "adjust": {"name": "adjustment"}, 441 | "1mo": {"name": "Mondays"}, 442 | "2tu": {"name": "Tuesdays"}, 443 | "3we": {"name": "Wednesdays"}, 444 | "4th": {"name": "Thursdays"}, 445 | "5fr": {"name": "Fridays"}, 446 | "6sa": {"name": "Saturdays"}, 447 | "7su": {"name": "Sundays"}, 448 | "start_time": {"name": "Begin"}, 449 | "end_time": {"name": "End"} 450 | } 451 | } 452 | } 453 | -------------------------------------------------------------------------------- /custom_components/waterkotte_heatpump/translations/de.json: -------------------------------------------------------------------------------- 1 | { 2 | "selector": { 3 | "system_type": { 4 | "options": { 5 | "ecotouch": "EcoTouch Modus [es werden Zugangsdaten in einem Anmeldeformular benötigt]", 6 | "easycon": "EasyCon Modus [älter Waterkotte Modelle (mit Basic-Auth Zugangsdaten)]" 7 | } 8 | }, 9 | "set_schedule_data_schedule_type": { 10 | "options": { 11 | "heating": "Heizung", 12 | "cooling": "Kühlung", 13 | "water": "Warmwasser", 14 | "pool": "Pool", 15 | "mix1": "Mischerkreis 1", 16 | "mix2": "Mischerkreis 2", 17 | "mix3": "Mischerkreis 3", 18 | "buffer_tank_circulation_pump": "Speicherentladepumpe", 19 | "solar": "Solarregelung", 20 | "pv": "Photovoltaik" 21 | } 22 | }, 23 | "set_schedule_data_schedule_days": { 24 | "options": { 25 | "1mo": "Montags", 26 | "2tu": "Dienstags", 27 | "3we": "Mittwochs", 28 | "4th": "Donnerstags", 29 | "5fr": "Freitags", 30 | "6sa": "Samstags", 31 | "7su": "Sonntags" 32 | } 33 | } 34 | }, 35 | "config": { 36 | "step": { 37 | "user": { 38 | "description": "Wenn Du Hilfe bei der Einichtung benötigst, findest du sie hier: https://github.com/marq24/ha-waterkotte.", 39 | "data": { 40 | "system_type": "Watterkotte Schnittstellentyp" 41 | }, 42 | "data_description": { 43 | "system_type": "Wenn Du zur Anmeldung an dem Waterkotte Webinterface ein Benutzernamen & Passwort angeben musst, dann wähle bitte den Schnittstellentyp: EcoTouch Modus, sonst den EasyCon Modus." 44 | } 45 | }, 46 | "user_ecotouch": { 47 | "description": "Wichtiger Hinweis: Mit dem 'Zeitprogramm anpassen Service' kannst Du alle Zeitpläne Deiner Waterkotte Wärmepumpe anpassen. Das Hinzufügen der optionalen Entitäten ist hierfür nicht notwendig. Diese werden nur dann benötigt, wenn Du die Zeitsteuerung über Dein Dashborad oder eine Deiner Automatisierungen modifizieren oder anzeigen möchtest.\r\rWenn Du Dir unsicher bist, füge die optionalen Zeitplanungs-Entitäten im ersten Schritt nicht hinzu. Sie können jederzeit über eine Einstellungen der Konfiguration zu einem späteren Zeitpunkt hinzugefügt werden.", 48 | "data": { 49 | "host": "IP oder Hostname deiner Waterkotte", 50 | "username": "Benutzername (bei den meisten Modellen ist dieser 'waterkotte')", 51 | "password": "Passwort", 52 | "polling_interval": "Aktualisierungsintervall in Sekunden", 53 | "tags_per_request": "Anzahl von TAGS die gleichzeitig angefordert werden (max. 75)", 54 | "add_schedule_entities": "Optionale Zeitsteuerung-Entitäten hinzufügen (650+)", 55 | "add_serial_as_id": "Möchtest Du mehrere Waterkotte-Systeme in deiner HA Instanz verwalten?" 56 | } 57 | }, 58 | "user_easycon": { 59 | "description": "Wenn Du Hilfe bei der Einichtung benötigst, findest du sie hier: https://github.com/marq24/ha-waterkotte.", 60 | "data": { 61 | "host": "IP oder Hostname deiner Waterkotte", 62 | "polling_interval": "Aktualisierungsintervall in Sekunden", 63 | "tags_per_request": "Anzahl von TAGS die gleichzeitig angefordert werden (max. 75)", 64 | "add_serial_as_id": "Möchtest Du mehrere Waterkotte-Systeme in deiner HA Instanz verwalten?" 65 | } 66 | }, 67 | "features": { 68 | "description": "Automatische Aktivierung zusätzlicher Sensoren und Steuerelemente:\n(Diese können auch später von Hand aktiviert werden)", 69 | "data": { 70 | "use_vent": "für eine 'Luftwärmepumpen-Außeneinheit'", 71 | "use_heating_curve": "zur Anpassung von 'Heizkurven'", 72 | "use_disinfection": "für die 'Warmwasser Desinfektion'", 73 | "use_pool": "für eine 'Pooleinheit' (inkl. Heizkurve)" 74 | } 75 | } 76 | }, 77 | "error": { 78 | "auth": "Unter dieser IP/Host und dem angegebenen Passwort konnte keine Waterkotte erreicht werden", 79 | "type": "Es konnte keine Verbindung mit deiner Waterkotte hergestellt werden - bist du dir sicher, dass du zuvor den richtigen Schnittstellentyp ausgewählt hast?" 80 | }, 81 | "abort": { 82 | "single_instance_allowed": "Only a single instance is allowed." 83 | } 84 | }, 85 | "options": { 86 | "step": { 87 | "user": { 88 | "description": "Wenn Du Hilfe bei der Einichtung benötigst, findest du sie hier: https://github.com/marq24/ha-waterkotte.\r\rWichtiger Hinweis: Mit dem 'Zeitprogramm anpassen Service' kannst Du alle Zeitpläne Deiner Waterkotte Heizung anpassen. Das Hinzufügen der optionalen Entitäten ist hierfür nicht notwendig. Diese werden nur dann benötigt, wenn Du die Zeitsteuerung über Dein Dashborad oder eine Deiner Automatisierungen modifizieren oder anzeigen möchtest.", 89 | "data": { 90 | "username": "Benutzername (bei den meisten Modellen ist dieser 'waterkotte')", 91 | "password": "Passwort", 92 | "polling_interval": "Aktualisierungsintervall in Sekunden", 93 | "tags_per_request": "Anzahl von TAGS die gleichzeitig angefordert werden (max. 75)", 94 | "add_schedule_entities": "Optionale Zeitsteuerungs-Entitäten hinzufügen (650+)" 95 | } 96 | } 97 | } 98 | }, 99 | "services": { 100 | "set_holiday": { 101 | "name": "Urlaubszeitraum setzen", 102 | "description": "Setzt den Start- & Endzeitpunkt des Urlaubsmodus", 103 | "fields": { 104 | "start": {"name": "Start Zeit & Datum", "description": "Urlaub beginnt am/um"}, 105 | "end": {"name": "Ende Zeit & Datum", "description": "Urlaub ended am/um"} 106 | } 107 | }, 108 | "set_disinfection_start_time": { 109 | "name": "Startzeit des Desinfektionslauf", 110 | "description": "Setze die Uhrzeit an dem der Desinfektionslauf an den ausgewählten Tagen startet", 111 | "fields": { 112 | "starthhmm": {"name": "Startzeit", "description": "Startzeit des Desinfektionslauf"} 113 | } 114 | }, 115 | "get_energy_balance": { 116 | "name": "Energiebilanz des laufenden Jahres ermitten", 117 | "description": "Ermittele die Energiebilanz nach unterschiedlichen Verbrauchern für das laufende Jahr" 118 | }, 119 | "get_energy_balance_monthly":{ 120 | "name": "Energiebilanz der letzten 12 Monate im Detail ermitten", 121 | "description": "Ermittele die Energiebilanz nach unterschiedlichen Verbrauchern in einem Fenster der letzten 12 Monate" 122 | }, 123 | "set_schedule_data": { 124 | "name": "Zeitprogramm anpassen", 125 | "description": "Anpassung eines Zeitprogramms für die Wärmepumpe (Start & Endzeitpunkte festlegen)", 126 | "fields": { 127 | "enable": {"name": "Zeitprogramm akivieren?"}, 128 | "schedule_type": {"name": "Typ", "description": "Welches Zeitprogramm soll angepasst werden?"}, 129 | "start_time": {"name": "Beginnt um"}, 130 | "end_time": {"name": "Endet um"}, 131 | "adj1_enable": {"name": "Anpassung I akivieren?"}, 132 | "adj1_value": {"name": "Anpassung I", "description": "Anhebung oder Absenkung"}, 133 | "adj1_start_time": {"name": "Anpassung I beginnt um"}, 134 | "adj1_end_time": {"name": "Anpassung I endet um"}, 135 | "adj2_enable": {"name": "Anpassung II akivieren?"}, 136 | "adj2_value": {"name": "Anpassung II", "description": "Anhebung oder Absenkung"}, 137 | "adj2_start_time": {"name": "Anpassung II beginnt um"}, 138 | "adj2_end_time": {"name": "Anpassung II endet um"}, 139 | "schedule_days": {"name": "Tage", "description": "An welchen Wochentagen?"} 140 | } 141 | } 142 | }, 143 | "entity": { 144 | "binary_sensor": { 145 | "state_sourcepump": {"name": "Quellenpumpe"}, 146 | "state_heatingpump": {"name": "Wärmepumpe"}, 147 | "state_evd": {"name": "EVD (Überhitzungsregler)"}, 148 | "state_compressor": {"name": "Verdichter"}, 149 | "state_compressor2": {"name": "Verdichter II"}, 150 | "state_external_heater": {"name": "Heizstab"}, 151 | "state_alarm": {"name": "Meldungen"}, 152 | "state_cooling": {"name": "Kühlen"}, 153 | "state_water": {"name": "Warmwasser"}, 154 | "state_pool": {"name": "Pool"}, 155 | "state_solar": {"name": "Solar"}, 156 | "state_cooling4way": {"name": "4-Wege Ventil"}, 157 | "status_heating": {"name": "Heizbetrieb"}, 158 | "status_water": {"name": "Warmwasserbetrieb"}, 159 | "status_cooling": {"name": "Kühlbetrieb"}, 160 | "status_pool": {"name": "Pool-Heizbetrieb"}, 161 | "status_solar": {"name": "Solarbetrieb"}, 162 | "status_heating_circulation_pump": {"name": "Umwälzpumpenbetrieb Heizung"}, 163 | "status_solar_circulation_pump": {"name": "Umwälzpumpenbetrieb Solar"}, 164 | "status_buffer_tank_circulation_pump": {"name": "Umwälzpumpenbetrieb Pufferspeicher"}, 165 | "status_compressor": {"name": "Verdichterbetrieb"}, 166 | "state_blocking_time": {"name": "Sperrzeit"}, 167 | "state_test_run": {"name": "Testlauf"}, 168 | "state_heating_circulation_pump_d425": {"name": "Umwälzpumpe Heizung"}, 169 | "state_buffertank_circulation_pump_d377": {"name": "Umwälzpumpe Pufferspeicher"}, 170 | "state_pool_circulation_pump_d549": {"name": "Umwälzpumpe Pool"}, 171 | "state_mix1_circulation_pump_d248": {"name": "Umwälzpumpe Mischer 1"}, 172 | "state_mix2_circulation_pump_d291": {"name": "Umwälzpumpe Mischer 2"}, 173 | "state_mix3_circulation_pump_d334": {"name": "Umwälzpumpe Mischer 3"}, 174 | "state_mix1_circulation_pump_d563": {"name": "Umwälzpumpe Mischer 1 [D563]"}, 175 | "basicvent_status_bypass_active_d1432": {"name": "Lüfter Bypass aktiv"}, 176 | "basicvent_status_humidifier_active_d1433": {"name": "Lüfter Luftbefeuchter aktiv"}, 177 | "basicvent_status_comfort_bypass_active_d1465": {"name": "Lüfter Komfort Bypass aktiv"}, 178 | "basicvent_status_smart_bypass_active_d1466": {"name": "Lüfter Smart Bypass aktiv"}, 179 | "basicvent_status_holiday_enabled_d1503": {"name": "Lüfter Ferienmodus aktiv"}, 180 | "basicvent_filter_change_display_d1469": {"name": "Lüfter Filterwechsel notwendig"} 181 | }, 182 | "number": { 183 | "temperature_return_setpoint": {"name": "T Sollwert"}, 184 | "temperature_cooling_setpoint": {"name": "Kühltemperatur"}, 185 | "temperature_cooling_outdoor_limit": {"name": "T Außen Einsatzgrenze"}, 186 | "temperature_cooling_flow_limit": {"name": "Vorlauftemperaturbegrenzung"}, 187 | "temperature_heating_setpoint": {"name": "Heiztemperatur"}, 188 | "temperature_heating_adjust": {"name": "Temperaturanpassung"}, 189 | "temperature_heating_hysteresis": {"name": "Schaltdifferenz Sollwert"}, 190 | "temperature_mix1_adjust": {"name": "Temperaturanpassung"}, 191 | "temperature_mix2_adjust": {"name": "Temperaturanpassung"}, 192 | "temperature_mix3_adjust": {"name": "Temperaturanpassung"}, 193 | "temperature_pool_adjust": {"name": "Temperaturanpassung"}, 194 | "temperature_heating_hc_limit": {"name": "T Heizgrenze"}, 195 | "temperature_heating_hc_target": {"name": "T Heizgrenze Soll"}, 196 | "temperature_heating_hc_outdoor_norm": {"name": "T Norm-Außen"}, 197 | "temperature_heating_hc_norm": {"name": "T Heizkreis Norm"}, 198 | "temperature_heating_setpointlimit_max": {"name": "Grenze für Sollwert (Max.)"}, 199 | "temperature_heating_setpointlimit_min": {"name": "Grenze für Sollwert (Min.)"}, 200 | "temperature_heating_powlimit_min": {"name": "Min. Heiz-Ausgang"}, 201 | "temperature_heating_powlimit_max": {"name": "Max. Heiz-Ausgang"}, 202 | "temperature_water_setpoint": {"name": "geforderte Temperatur"}, 203 | "temperature_water_hysteresis": {"name": "Schaltdifferenz Sollwert"}, 204 | "temperature_water_powlimit_min": {"name": "Min. Warmwasser-Ausgang"}, 205 | "temperature_water_powlimit_max": {"name": "Max. Warmwasser-Ausgang"}, 206 | "temperature_mix1_hc_limit": {"name": "T Heizgrenze"}, 207 | "temperature_mix1_hc_target": {"name": "T Heizgrenze Soll"}, 208 | "temperature_mix1_hc_outdoor_norm": {"name": "T Norm-Außen"}, 209 | "temperature_mix1_hc_heating_norm": {"name": "T Heizkreis Norm"}, 210 | "temperature_mix1_hc_max": {"name": "Maximale Temperatur im Mischerkreis"}, 211 | "temperature_mix2_hc_limit": {"name": "T Heizgrenze"}, 212 | "temperature_mix2_hc_target": {"name": "T Heizgrenze Soll"}, 213 | "temperature_mix2_hc_outdoor_norm": {"name": "T Norm-Außen"}, 214 | "temperature_mix2_hc_heating_norm": {"name": "T Heizkreis Norm"}, 215 | "temperature_mix2_hc_max": {"name": "Maximale Temperatur im Mischerkreis"}, 216 | "temperature_mix3_hc_limit": {"name": "T Heizgrenze"}, 217 | "temperature_mix3_hc_target": {"name": "T Heizgrenze Soll"}, 218 | "temperature_mix3_hc_outdoor_norm": {"name": "T Norm-Außen"}, 219 | "temperature_mix3_hc_heating_norm": {"name": "T Heizkreis Norm"}, 220 | "temperature_mix3_hc_max": {"name": "Maximale Temperatur im Mischerkreis"}, 221 | "temperature_water_disinfection": {"name": "geforderte Temperatur"}, 222 | "schedule_water_disinfection_duration": {"name": "max.Laufzeit"}, 223 | "temperature_pool_setpoint": {"name": "geforderte Temperatur"}, 224 | "temperature_pool_hysteresis": {"name": "Schaltdifferenz Sollwert"}, 225 | "temperature_pool_hc_limit": {"name": "T Heizgrenze"}, 226 | "temperature_pool_hc_target": {"name": "T Heizgrenze Soll"}, 227 | "temperature_pool_hc_outdoor_norm": {"name": "T Norm-Außen"}, 228 | "temperature_pool_hc_norm": {"name": "T Heizkreis Norm"}, 229 | "temperature_pool_max_runtime": {"name": "Maximale Laufzeit Pool-Heizbetrieb"}, 230 | "temperature_pool_setpointlimit": {"name": "Grenze für Pool Sollwert"}, 231 | "temperature_pool_powlimit_min": {"name": "Min. Pool-Ausgang"}, 232 | "temperature_pool_powlimit_max": {"name": "Max. Pool-Ausgang"}, 233 | "basicvent_incoming_fan_manual_speed_percent": {"name": "Lüfter 1 (Außen) manuelle Drehzahl"}, 234 | "basicvent_outgoing_fan_manual_speed_percent": {"name": "Lüfter 2 (Innen) manuelle Drehzahl"}, 235 | "pumpservice_sourcepump_pre_runtime_i1278": {"name": "Service: Quellenpumpe Vorlaufzeit"}, 236 | "pumpservice_sourcepump_post_runtime_i1279": {"name": "Service: Quellenpumpe Nachlaufzeit"}, 237 | "pumpservice_sourcepump_anti_jamming_i1280": {"name": "Service: Quellenpumpe Festsitzschutz"}, 238 | "pumpservice_sourcepump_temp_on_lower_a1539": {"name": "Service: Quellenpumpe Temp. Quelle Ein <"}, 239 | "pumpservice_sourcepump_heatmode_minspeed_a485": {"name": "Service: Quellenpumpe Heizbetrieb Min. Drehzahl"}, 240 | "pumpservice_sourcepump_heatmode_maxspeed_a486": {"name": "Service: Quellenpumpe Heizbetrieb Max. Drehzahl"}, 241 | "pumpservice_sourcepump_heatmode_source_temperature_a479": {"name": "Service: Quellenpumpe Heizbetrieb ΔT Wärmequelle"}, 242 | "pumpservice_sourcepump_coolingmode_minspeed_a1032": {"name": "Service: Quellenpumpe Kühlbetrieb Min. Drehzahl"}, 243 | "pumpservice_sourcepump_coolingmode_maxspeed_a1033": {"name": "Service: Quellenpumpe Kühlbetrieb Max. Drehzahl"}, 244 | "pumpservice_sourcepump_coolingmode_source_temperature_a1034": {"name": "Service: Quellenpumpe Kühlbetrieb Temp. Wärmequelle"}, 245 | "temperature_room_target_a100": {"name": "T Raum-Sollwert"} 246 | }, 247 | "select": { 248 | "temperature_heating_mode": { 249 | "name": "Heizungsregelung", 250 | "state": { 251 | "mode0": "Witterungsgeführt", 252 | "mode1": "Manuelle Sollwertvorgabe", 253 | "mode2": "Sollwertvorgabe BMS", 254 | "mode3": "Sollwertvorgabe EXT", 255 | "mode4": "Sollwertvorgabe 0-10V", 256 | "mode5": "Mischerkreis Vorgabe" 257 | } 258 | }, 259 | "temperature_pool_mode": { 260 | "name": "Poolregelung", 261 | "state": { 262 | "mode0": "Witterungsgeführt", 263 | "mode1": "Sollwertvorgabe", 264 | "mode2": "Sollwertvorgabe BMS", 265 | "mode3": "Sollwertvorgabe EXT" 266 | } 267 | }, 268 | "basicvent_operation_mode_i4582": { 269 | "name": "Lüfter Betriebsmodus (I4582)", 270 | "state": { 271 | "mode0": "Tag", 272 | "mode1": "Nacht", 273 | "mode2": "Zeitprogramm", 274 | "mode3": "Party", 275 | "mode4": "Urlaub", 276 | "mode5": "Bypass" 277 | } 278 | }, 279 | "basicvent_operation_mode_alt": { 280 | "name": "Lüfter Betriebsmodus (alternative)", 281 | "state": { 282 | "mode0": "Tag", 283 | "mode1": "Nacht", 284 | "mode2": "Zeitprogramm", 285 | "mode3": "Party", 286 | "mode4": "Urlaub", 287 | "mode5": "Bypass" 288 | } 289 | }, 290 | "enable_cooling": {"name": "Betriebsmodus Kühlung"}, 291 | "enable_heating": {"name": "Betriebsmodus Heizung"}, 292 | "enable_pv": {"name": "Betriebsmodus PV"}, 293 | "enable_warmwater": {"name": "Betriebsmodus Warmwasser"}, 294 | "enable_pool": {"name": "Betriebsmodus Pool"}, 295 | "enable_external_heater": {"name": "Betriebsmodus Heizstab"}, 296 | "enable_mixing1": {"name": "Betriebsmodus Mischerkreis 1"}, 297 | "enable_mixing2": {"name": "Betriebsmodus Mischerkreis 2"}, 298 | "enable_mixing3": {"name": "Betriebsmodus Mischerkreis 3"}, 299 | "pumpservice_sourcepump_i1281": { 300 | "name": "Service: Quellenpumpe", 301 | "state": { 302 | "0": "Aus", 303 | "1": "Ein", 304 | "2": "Auto" 305 | } 306 | }, 307 | "pumpservice_sourcepump_mode_i1764": { 308 | "name": "Service: Quellenpumpe Modus", 309 | "state": { 310 | "0": "0-10V", 311 | "1": "PWM T12", 312 | "2": "PWM T13" 313 | } 314 | }, 315 | "pumpservice_sourcepump_heatmode_regulation_by_i1752": { 316 | "name": "Service: Quellenpumpe Heizbetrieb Regelung nach…", 317 | "state": { 318 | "0": "Spreizung", 319 | "1": "Temperatur" 320 | } 321 | }, 322 | "pumpservice_sourcepump_heatmode_control_behaviour_d789": { 323 | "name": "Service: Quellenpumpe Heizbetrieb Regelverhalten", 324 | "state": { 325 | "0": "Standard", 326 | "1": "Invertiert" 327 | } 328 | }, 329 | "pumpservice_sourcepump_heatmode_regulation_start_d996": { 330 | "name": "Service: Quellenpumpe Heizbetrieb Begin der Regelung mit…", 331 | "state": { 332 | "0": "Min. Drehzahl", 333 | "1": "Max. Drehzahl" 334 | } 335 | }, 336 | "pumpservice_sourcepump_coolingmode_regulation_by_i2102": { 337 | "name": "Service: Quellenpumpe Kühlbetrieb Regelung nach…", 338 | "state": { 339 | "0": "Spreizung", 340 | "1": "Temperatur", 341 | "2": "Temperatur Sekundär" 342 | } 343 | }, 344 | "pumpservice_sourcepump_coolingmode_control_behaviour_d995": { 345 | "name": "Service: Quellenpumpe Kühlbetrieb Regelverhalten", 346 | "state": { 347 | "0": "Standard", 348 | "1": "Invertiert" 349 | } 350 | }, 351 | "pumpservice_sourcepump_coolingmode_regulation_start_d997": { 352 | "name": "Service: Quellenpumpe Kühlbetrieb Begin der Regelung mit…", 353 | "state": { 354 | "0": "Min. Drehzahl", 355 | "1": "Max. Drehzahl" 356 | } 357 | }, 358 | "room_influence_a101_or_i264": { 359 | "name": "Raumeinfluss", 360 | "state": { 361 | "0": "0%", 362 | "1": "50%", 363 | "2": "100%", 364 | "3": "150%", 365 | "4": "200%" 366 | } 367 | } 368 | }, 369 | "sensor": { 370 | "energy_consumption_total_year": {"name": "Elektrische Arbeit Jahresverbrauch"}, 371 | "compressor_electric_consumption_year": {"name": "Verdichter Jahresverbrauch"}, 372 | "sourcepump_electric_consumption_year": {"name": "Quellenpumpe Jahresverbrauch"}, 373 | "electrical_heater_electric_consumption_year": {"name": "Heizstab Jahresverbrauch"}, 374 | "energy_production_total_year": {"name": "Thermische Energie Jahresproduktion"}, 375 | "heating_energy_production_year": {"name": "Heizenergie Jahresproduktion"}, 376 | "hot_water_energy_production_year": {"name": "Warmwasserenergie Jahresproduktion"}, 377 | "pool_energy_production_year": {"name": "Poolenergie Jahresproduktion"}, 378 | "cooling_energy_year": {"name": "Kältearbeit im Jahr"}, 379 | "temperature_outside": {"name": "Außentemperatur"}, 380 | "temperature_outside_1h": {"name": "Außentemperatur 1h"}, 381 | "temperature_outside_24h": {"name": "Außentemperatur 24h"}, 382 | "temperature_source_entry": {"name": "T Quelle Ein"}, 383 | "temperature_source_exit": {"name": "T Quelle Aus"}, 384 | "temperature_evaporation": {"name": "T Verdampfer"}, 385 | "temperature_suction_line": {"name": "T Saugleitung"}, 386 | "temperature_return": {"name": "T Rücklauf"}, 387 | "temperature_flow": {"name": "T Vorlauf"}, 388 | "temperature_condensation": {"name": "T Kondensation"}, 389 | "temperature_buffertank": {"name": "Temperatur Pufferspeicher"}, 390 | "temperature_room": {"name": "Temperatur Raum"}, 391 | "temperature_room_1h": {"name": "Temperatur Raum 1h"}, 392 | "temperature_heating": {"name": "aktuelle Temperatur"}, 393 | "temperature_heating_demand": {"name": "geforderte Temperatur"}, 394 | "temperature_cooling": {"name": "aktuelle Temperatur"}, 395 | "temperature_cooling_demand": {"name": "aktuelle Temperatur"}, 396 | "temperature_water": {"name": "Temperatur Warmwasser"}, 397 | "temperature_water_demand": {"name": "geforderte Temperatur"}, 398 | "temperature_mix1": {"name": "aktuelle Temperatur"}, 399 | "temperature_mix1_percent": {"name": "Y"}, 400 | "temperature_mix1_demand": {"name": "geforderte Temperatur"}, 401 | "temperature_mix2": {"name": "aktuelle Temperatur"}, 402 | "temperature_mix2_percent": {"name": "Y"}, 403 | "temperature_mix2_demand": {"name": "geforderte Temperatur"}, 404 | "temperature_mix3": {"name": "aktuelle Temperatur"}, 405 | "temperature_mix3_percent": {"name": "Y"}, 406 | "temperature_mix3_demand": {"name": "geforderte Temperatur"}, 407 | "temperature_pool": {"name": "aktuelle Temperatur"}, 408 | "temperature_pool_demand": {"name": "geforderte Temperatur"}, 409 | "temperature_solar": {"name": "T Solar"}, 410 | "temperature_solar_exit": {"name": "Austrittstemperatur Solarkollektor"}, 411 | "temperature_discharge": {"name": "Druckgastemperatur"}, 412 | "pressure_evaporation": {"name": "p Verdampfer"}, 413 | "pressure_condensation": {"name": "p Kondensator"}, 414 | "pressure_water": {"name": "Wasserdruck"}, 415 | "position_expansion_valve": {"name": "EEV Ventilöffnung"}, 416 | "suction_gas_overheating": {"name": "Sauggas Überhitzung"}, 417 | "power_electric": {"name": "Elektrische Leistung"}, 418 | "power_heating": {"name": "Thermische Leistung"}, 419 | "power_cooling": {"name": "Kälteleistung"}, 420 | "cop_heating": {"name": "COP"}, 421 | "cop_cooling": {"name": "COP Kälteleistung"}, 422 | "percent_heat_circ_pump": {"name": "Drehzahl Heizungspumpe"}, 423 | "percent_source_pump": {"name": "Drehzahl Quellenpumpe"}, 424 | "percent_compressor": {"name": "Leistung Verdichter"}, 425 | "waterkotte_bios_time": {"name": "BIOS Zeit"}, 426 | "holiday_start_time": {"name": "Urlaub Start"}, 427 | "holiday_end_time": {"name": "Urlaub Ende"}, 428 | "schedule_water_disinfection_start_time": {"name": "Startzeit"}, 429 | "state_service": {"name": "Servicedaten"}, 430 | "alarm_bits": {"name": "Störungen"}, 431 | "interruption_bits": {"name": "Unterbrechungen"}, 432 | "basicvent_filter_change_operating_days_a4498": {"name": "Luftfilter-Wechsel Betriebsstunden"}, 433 | "basicvent_filter_change_remaining_operating_days_a4504": {"name": "Luftfilter-Wechsel Betriebsstunden Restlaufzeit"}, 434 | "basicvent_humidity_value_a4990": {"name": "Lüfter Luftfeuchtigkeit"}, 435 | "basicvent_co2_value_a4992": {"name": "Lüfter CO2-Konzentration"}, 436 | "basicvent_voc_value_a4522": {"name": "Lüfter VOC Kohlenwasserstoffverbindungen"}, 437 | "basicvent_incoming_fan_rpm_a4551": {"name": "Lüfter 1 (Außen) Umdrehungen"}, 438 | "basicvent_incoming_fan_a4986": {"name": "Lüfter 1 (Außen) Leistung"}, 439 | "basicvent_temperature_incoming_air_before_oda_a5000": {"name": "Lüfter Frischluft (ODA) Temperatur"}, 440 | "basicvent_temperature_incoming_air_after_sup_a4996": {"name": "Lüfter Zuluft (SUP) Temperatur"}, 441 | "basicvent_outgoing_fan_rpm_a4547": {"name": "Lüfter 2 (Innen) Umdrehungen"}, 442 | "basicvent_outgoing_fan_a4984": {"name": "Lüfter 2 (Innen) Leistung"}, 443 | "basicvent_temperature_outgoing_air_before_eth_a4998": {"name": "Lüfter Abluft (ETH) Temperatur"}, 444 | "basicvent_temperature_outgoing_air_after_eeh_a4994": {"name": "Lüfter Fortluft (EEH) Temperatur"}, 445 | "basicvent_energy_save_total_a4387": {"name": "Lüfter Energieersparnis gesamt"}, 446 | "basicvent_energy_save_current_a4389": {"name": "Lüfter Energieersparnis aktuell"}, 447 | "basicvent_energy_recovery_rate_a4391": {"name": "Lüfter Wärmerückgewinnungsgrad"}, 448 | "operating_hours_compressor_1": {"name": "Betriebsstunden Verdichter"}, 449 | "operating_hours_compressor_2": {"name": "Betriebsstunden Verdichter II"}, 450 | "operating_hours_circulation_pump": {"name": "Betriebsstunden Umwälzpumpe"}, 451 | "operating_hours_source_pump": {"name": "Betriebsstunden Quellenpumpe"}, 452 | "operating_hours_solar": {"name": "Betriebsstunden Solar"}, 453 | "temperature_room_1h_a98": {"name": "Temperatur Raum 1h (alt)"} 454 | }, 455 | "switch": { 456 | "holiday_enabled": {"name": "Urlaubsfunktion"}, 457 | "schedule_water_disinfection_1mo": {"name": "Montag"}, 458 | "schedule_water_disinfection_2tu": {"name": "Dienstag"}, 459 | "schedule_water_disinfection_3we": {"name": "Mittwoch"}, 460 | "schedule_water_disinfection_4th": {"name": "Donnerstag"}, 461 | "schedule_water_disinfection_5fr": {"name": "Freitag"}, 462 | "schedule_water_disinfection_6sa": {"name": "Samstag"}, 463 | "schedule_water_disinfection_7su": {"name": "Sonntag"}, 464 | "permanent_heating_circulation_pump_winter_d1103": {"name": "Dauerbetrieb Heizungsumwälzpumpe während Heizperiode"}, 465 | "permanent_heating_circulation_pump_summer_d1104": {"name": "Dauerbetrieb Heizungsumwälzpumpe während Kühlperiode"}, 466 | "basicvent_filter_change_operating_hours_reset_d1544": {"name": "Luftfilter-Wechsel Betriebsstunden ZURÜCKSETZEN"}, 467 | "basicvent_incoming_fan_manual_mode": {"name": "Lüfter 1 (Außen) manuelle Drehzahl-Steuerung"}, 468 | "basicvent_outgoing_fan_manual_mode": {"name": "Lüfter 2 (Innen) manuelle Drehzahl-Steuerung"}, 469 | "pumpservice_sourcepump_cable_break_monitoring_d881": {"name": "Service: Quellenpumpe Kabelbruchüberwachung"}, 470 | "pumpservice_sourcepump_regeneration_d1294": {"name": "Service: Quellenpumpe Regeneration"} 471 | }, 472 | "code_gen": { 473 | "schedule": {"name": "Zeitprogramm"}, 474 | "heating": {"name": "Heizung"}, 475 | "cooling": {"name": "Kühlung"}, 476 | "water": {"name": "Warmwasser"}, 477 | "pool": {"name": "Pool"}, 478 | "mix1": {"name": "Mischerkreis 1"}, 479 | "mix2": {"name": "Mischerkreis 2"}, 480 | "mix3": {"name": "Mischerkreis 3"}, 481 | "buffer_tank_circulation_pump": {"name": "Speicherentladepumpe"}, 482 | "solar": {"name": "Solarregelung"}, 483 | "pv": {"name": "Photovoltaik"}, 484 | "adjust": {"name": "Anpassung"}, 485 | "1mo": {"name": "Montags"}, 486 | "2tu": {"name": "Dienstags"}, 487 | "3we": {"name": "Mittwochs"}, 488 | "4th": {"name": "Donnerstags"}, 489 | "5fr": {"name": "Freitags"}, 490 | "6sa": {"name": "Samstags"}, 491 | "7su": {"name": "Sonntags"}, 492 | "start_time": {"name": "Begin"}, 493 | "end_time": {"name": "Ende"} 494 | } 495 | } 496 | } -------------------------------------------------------------------------------- /custom_components/waterkotte_heatpump/service.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | import logging 3 | import asyncio 4 | from typing import Tuple 5 | from dateutil.relativedelta import relativedelta 6 | from homeassistant.core import ServiceCall, ServiceResponse 7 | 8 | from custom_components.waterkotte_heatpump.pywaterkotte_ha.tags import WKHPTag 9 | 10 | _LOGGER: logging.Logger = logging.getLogger(__package__) 11 | 12 | 13 | class WaterkotteHeatpumpService(): 14 | """waterkotte_heatpump switch class.""" 15 | 16 | def __init__(self, hass, config, coordinator): # pylint: disable=unused-argument 17 | """Initialize the sensor.""" 18 | self._hass = hass 19 | self._config = config 20 | self._coordinator = coordinator 21 | 22 | async def sync_time(self, call: ServiceCall): 23 | now = datetime.datetime.now() 24 | next_full_minute = now.replace(second=0, microsecond=0) + relativedelta(minutes=1) 25 | if now.second < 58: 26 | await asyncio.sleep(58 - now.second) 27 | # await self._coordinator.async_write_tag(WKHPTag.WATERKOTTE_TIME_SET, next_full_minute) 28 | if call.return_response: 29 | return {"success": "yes", "date": str(next_full_minute)} 30 | 31 | async def set_holiday(self, call: ServiceCall): 32 | """Handle the service call.""" 33 | start = call.data.get('start', None) 34 | end = call.data.get('end', None) 35 | if start is not None and end is not None: 36 | start = datetime.datetime.strptime(start, '%Y-%m-%d %H:%M:%S') 37 | end = datetime.datetime.strptime(end, '%Y-%m-%d %H:%M:%S') 38 | _LOGGER.debug(f"set_holiday start: {start} end: {end}") 39 | try: 40 | await self._coordinator.async_write_tag(WKHPTag.HOLIDAY_START_TIME, start) 41 | await self._coordinator.async_write_tag(WKHPTag.HOLIDAY_END_TIME, end) 42 | await self._coordinator.async_refresh() 43 | except ValueError as exc: 44 | if call.return_response: 45 | return {"error": str(exc), "date": str(datetime.datetime.now().time())} 46 | 47 | if call.return_response: 48 | return {"success": "yes", "date": str(datetime.datetime.now().time())} 49 | 50 | if call.return_response: 51 | return {"error": "No Start and/or End Time", "date": str(datetime.datetime.now().time())} 52 | 53 | async def set_disinfection_start_time(self, call: ServiceCall): 54 | start_time = self._get_time("starthhmm", call) 55 | if start_time is not None: 56 | _LOGGER.debug(f"set_disinfection_start_time: {start_time}") 57 | try: 58 | await self._coordinator.async_write_tag(WKHPTag.SCHEDULE_WATER_DISINFECTION_START_TIME, start_time) 59 | await self._coordinator.async_refresh() 60 | if call.return_response: 61 | return { 62 | "success": "yes", 63 | "date": str(datetime.datetime.now().time()) 64 | } 65 | except ValueError as exe: 66 | if call.return_response: 67 | return {"error": str(exe), "date": str(datetime.datetime.now().time())} 68 | else: 69 | if call.return_response: 70 | return {"error": "no start_time provided", "date": str(datetime.datetime.now().time())} 71 | 72 | async def set_schedule_data(self, call: ServiceCall): 73 | type = call.data.get("schedule_type", None) 74 | days = call.data.get("schedule_days", None) 75 | enable = call.data.get("enable", None) 76 | start_time = self._get_time("start_time", call) 77 | end_time = self._get_time("end_time", call) 78 | 79 | no_adj_values = type == "solar" or type == "pv" 80 | if not no_adj_values: 81 | adj1_enable = call.data.get("adj1_enable", None) 82 | adj1_value = call.data.get("adj1_value", None) 83 | adj1_start_time = self._get_time("adj1_start_time", call) 84 | adj1_end_time = self._get_time("adj1_end_time", call) 85 | 86 | adj2_enable = call.data.get("adj2_enable", None) 87 | adj2_value = call.data.get("adj2_value", None) 88 | adj2_start_time = self._get_time("adj2_start_time", call) 89 | adj2_end_time = self._get_time("adj2_end_time", call) 90 | 91 | if type is not None and days is not None and len(days) > 0: 92 | try: 93 | kv_pairs = [] 94 | final_type = f"SCHEDULE_{type.upper()}" 95 | for a_day in days: 96 | if enable is not None: 97 | kv_pairs.append((WKHPTag[f"{final_type}_{a_day.upper()}_ENABLE"], enable)) 98 | if start_time is not None: 99 | kv_pairs.append((WKHPTag[f"{final_type}_{a_day.upper()}_START_TIME"], start_time)) 100 | if end_time is not None: 101 | kv_pairs.append((WKHPTag[f"{final_type}_{a_day.upper()}_END_TIME"], end_time)) 102 | 103 | if not no_adj_values: 104 | if adj1_enable is not None: 105 | kv_pairs.append((WKHPTag[f"{final_type}_{a_day.upper()}_ADJUST1_ENABLE"], adj1_enable)) 106 | if adj1_value is not None: 107 | kv_pairs.append((WKHPTag[f"{final_type}_{a_day.upper()}_ADJUST1_VALUE"], float(adj1_value))) 108 | if adj1_start_time is not None: 109 | kv_pairs.append((WKHPTag[f"{final_type}_{a_day.upper()}_ADJUST1_START_TIME"], adj1_start_time)) 110 | if adj1_end_time is not None: 111 | kv_pairs.append((WKHPTag[f"{final_type}_{a_day.upper()}_ADJUST1_END_TIME"], adj1_end_time)) 112 | 113 | if adj2_enable is not None: 114 | kv_pairs.append((WKHPTag[f"{final_type}_{a_day.upper()}_ADJUST1_ENABLE"], adj2_enable)) 115 | if adj2_value is not None: 116 | kv_pairs.append((WKHPTag[f"{final_type}_{a_day.upper()}_ADJUST2_VALUE"], float(adj2_value))) 117 | if adj2_start_time is not None: 118 | kv_pairs.append((WKHPTag[f"{final_type}_{a_day.upper()}_ADJUST2_START_TIME"], adj1_start_time)) 119 | if adj2_end_time is not None: 120 | kv_pairs.append((WKHPTag[f"{final_type}_{a_day.upper()}_ADJUST2_END_TIME"], adj1_end_time)) 121 | 122 | _LOGGER.debug(f"set_schedule_data for: '{type}' @{days} -> {kv_pairs}") 123 | await self._coordinator.async_write_tags(kv_pairs) 124 | await self._coordinator.async_refresh() 125 | except ValueError as exe: 126 | if call.return_response: 127 | return {"error": str(exe), "date": str(datetime.datetime.now().time())} 128 | if call.return_response: 129 | return { 130 | "success": "yes", 131 | "type": final_type, 132 | "count": len(kv_pairs), 133 | "date": str(datetime.datetime.now().time()) 134 | } 135 | else: 136 | if call.return_response: 137 | return {"error": "no type or day provided", "date": str(datetime.datetime.now().time())} 138 | 139 | def _get_time(self, key: str, call: ServiceCall): 140 | a_time = call.data.get(key, None) 141 | if a_time is not None: 142 | temp = str(a_time) 143 | if temp.startswith("24:"): 144 | #temp = "00" + temp[2:] 145 | #return datetime.time.fromisoformat(temp).max 146 | return datetime.time.max 147 | else: 148 | return datetime.time.fromisoformat(temp) 149 | return None 150 | 151 | async def get_energy_balance(self, call: ServiceCall) -> ServiceResponse: 152 | try: 153 | tags = [WKHPTag.COMPRESSOR_ELECTRIC_CONSUMPTION_YEAR, 154 | WKHPTag.SOURCEPUMP_ELECTRIC_CONSUMPTION_YEAR, 155 | WKHPTag.ELECTRICAL_HEATER_ELECTRIC_CONSUMPTION_YEAR, 156 | WKHPTag.HEATING_ENERGY_PRODUCTION_YEAR, 157 | WKHPTag.HOT_WATER_ENERGY_PRODUCTION_YEAR, 158 | WKHPTag.POOL_ENERGY_PRODUCTION_YEAR, 159 | WKHPTag.COP_HEATPUMP_YEAR, 160 | WKHPTag.COP_HEATPUMP_ACTUAL_YEAR_INFO] 161 | res = await self._coordinator.async_read_values(tags) 162 | 163 | except ValueError: 164 | return "unavailable" 165 | ret = { 166 | "year": res.get(WKHPTag.COP_HEATPUMP_ACTUAL_YEAR_INFO, {"value": "unknown"})["value"], 167 | "cop": res.get(WKHPTag.COP_HEATPUMP_YEAR, {"value": "unknown"})["value"], 168 | "compressor": res.get(WKHPTag.COMPRESSOR_ELECTRIC_CONSUMPTION_YEAR, {"value": "unknown"})["value"], 169 | "sourcepump": res.get(WKHPTag.SOURCEPUMP_ELECTRIC_CONSUMPTION_YEAR, {"value": "unknown"})["value"], 170 | "externalheater": res.get(WKHPTag.ELECTRICAL_HEATER_ELECTRIC_CONSUMPTION_YEAR, {"value": "unknown"})[ 171 | "value"], 172 | "heating": res.get(WKHPTag.HEATING_ENERGY_PRODUCTION_YEAR, {"value": "unknown"})["value"], 173 | "warmwater": res.get(WKHPTag.HOT_WATER_ENERGY_PRODUCTION_YEAR, {"value": "unknown"})["value"], 174 | "pool": res.get(WKHPTag.POOL_ENERGY_PRODUCTION_YEAR, {"value": "unknown"})["value"]} 175 | return ret 176 | 177 | async def get_energy_balance_monthly(self, call: ServiceCall) -> ServiceResponse: 178 | try: 179 | for x in range(2): 180 | tags = [WKHPTag.ENG_CONSUMPTION_COMPRESSOR01, 181 | WKHPTag.ENG_CONSUMPTION_COMPRESSOR02, 182 | WKHPTag.ENG_CONSUMPTION_COMPRESSOR03, 183 | WKHPTag.ENG_CONSUMPTION_COMPRESSOR04, 184 | WKHPTag.ENG_CONSUMPTION_COMPRESSOR05, 185 | WKHPTag.ENG_CONSUMPTION_COMPRESSOR06, 186 | WKHPTag.ENG_CONSUMPTION_COMPRESSOR07, 187 | WKHPTag.ENG_CONSUMPTION_COMPRESSOR08, 188 | WKHPTag.ENG_CONSUMPTION_COMPRESSOR09, 189 | WKHPTag.ENG_CONSUMPTION_COMPRESSOR10, 190 | WKHPTag.ENG_CONSUMPTION_COMPRESSOR11, 191 | WKHPTag.ENG_CONSUMPTION_COMPRESSOR12] 192 | resCompressor = await self._coordinator.async_read_values(tags) 193 | found = False 194 | for value in resCompressor: 195 | if resCompressor.get(value, {"value": "unknown"})["value"] == "unknown": 196 | found = True 197 | if len(resCompressor) == 12 and not found: 198 | break 199 | for x in range(2): 200 | tags = [WKHPTag.ENG_CONSUMPTION_SOURCEPUMP01, 201 | WKHPTag.ENG_CONSUMPTION_SOURCEPUMP02, 202 | WKHPTag.ENG_CONSUMPTION_SOURCEPUMP03, 203 | WKHPTag.ENG_CONSUMPTION_SOURCEPUMP04, 204 | WKHPTag.ENG_CONSUMPTION_SOURCEPUMP05, 205 | WKHPTag.ENG_CONSUMPTION_SOURCEPUMP06, 206 | WKHPTag.ENG_CONSUMPTION_SOURCEPUMP07, 207 | WKHPTag.ENG_CONSUMPTION_SOURCEPUMP08, 208 | WKHPTag.ENG_CONSUMPTION_SOURCEPUMP09, 209 | WKHPTag.ENG_CONSUMPTION_SOURCEPUMP10, 210 | WKHPTag.ENG_CONSUMPTION_SOURCEPUMP11, 211 | WKHPTag.ENG_CONSUMPTION_SOURCEPUMP12] 212 | resSourcePump = await self._coordinator.async_read_values(tags) 213 | found = False 214 | for value in resSourcePump: 215 | if resSourcePump.get(value, {"value": "unknown"})["value"] == "unknown": 216 | found = True 217 | if len(resSourcePump) == 12 and not found: 218 | break 219 | for x in range(2): 220 | tags = [WKHPTag.ENG_CONSUMPTION_EXTERNALHEATER01, 221 | WKHPTag.ENG_CONSUMPTION_EXTERNALHEATER02, 222 | WKHPTag.ENG_CONSUMPTION_EXTERNALHEATER03, 223 | WKHPTag.ENG_CONSUMPTION_EXTERNALHEATER04, 224 | WKHPTag.ENG_CONSUMPTION_EXTERNALHEATER05, 225 | WKHPTag.ENG_CONSUMPTION_EXTERNALHEATER06, 226 | WKHPTag.ENG_CONSUMPTION_EXTERNALHEATER07, 227 | WKHPTag.ENG_CONSUMPTION_EXTERNALHEATER08, 228 | WKHPTag.ENG_CONSUMPTION_EXTERNALHEATER09, 229 | WKHPTag.ENG_CONSUMPTION_EXTERNALHEATER10, 230 | WKHPTag.ENG_CONSUMPTION_EXTERNALHEATER11, 231 | WKHPTag.ENG_CONSUMPTION_EXTERNALHEATER12] 232 | resExternalHeater = await self._coordinator.async_read_values(tags) 233 | found = False 234 | for value in resExternalHeater: 235 | if resExternalHeater.get(value, {"value": "unknown"})["value"] == "unknown": 236 | found = True 237 | if len(resExternalHeater) == 12 and not found: 238 | break 239 | for x in range(2): 240 | tags = [WKHPTag.ENG_PRODUCTION_HEATING01, 241 | WKHPTag.ENG_PRODUCTION_HEATING02, 242 | WKHPTag.ENG_PRODUCTION_HEATING03, 243 | WKHPTag.ENG_PRODUCTION_HEATING04, 244 | WKHPTag.ENG_PRODUCTION_HEATING05, 245 | WKHPTag.ENG_PRODUCTION_HEATING06, 246 | WKHPTag.ENG_PRODUCTION_HEATING07, 247 | WKHPTag.ENG_PRODUCTION_HEATING08, 248 | WKHPTag.ENG_PRODUCTION_HEATING09, 249 | WKHPTag.ENG_PRODUCTION_HEATING10, 250 | WKHPTag.ENG_PRODUCTION_HEATING11, 251 | WKHPTag.ENG_PRODUCTION_HEATING12] 252 | resHeater = await self._coordinator.async_read_values(tags) 253 | found = False 254 | for value in resHeater: 255 | if resHeater.get(value, {"value": "unknown"})["value"] == "unknown": 256 | found = True 257 | if len(resHeater) == 12 and not found: 258 | break 259 | for x in range(2): 260 | tags = [WKHPTag.ENG_PRODUCTION_WARMWATER01, 261 | WKHPTag.ENG_PRODUCTION_WARMWATER02, 262 | WKHPTag.ENG_PRODUCTION_WARMWATER03, 263 | WKHPTag.ENG_PRODUCTION_WARMWATER04, 264 | WKHPTag.ENG_PRODUCTION_WARMWATER05, 265 | WKHPTag.ENG_PRODUCTION_WARMWATER06, 266 | WKHPTag.ENG_PRODUCTION_WARMWATER07, 267 | WKHPTag.ENG_PRODUCTION_WARMWATER08, 268 | WKHPTag.ENG_PRODUCTION_WARMWATER09, 269 | WKHPTag.ENG_PRODUCTION_WARMWATER10, 270 | WKHPTag.ENG_PRODUCTION_WARMWATER11, 271 | WKHPTag.ENG_PRODUCTION_WARMWATER12] 272 | resWarmWater = await self._coordinator.async_read_values(tags) 273 | found = False 274 | for value in resWarmWater: 275 | if resWarmWater.get(value, {"value": "unknown"})["value"] == "unknown": 276 | found = True 277 | if len(resWarmWater) == 12 and not found: 278 | break 279 | for x in range(2): 280 | tags = [WKHPTag.ENG_PRODUCTION_POOL01, 281 | WKHPTag.ENG_PRODUCTION_POOL02, 282 | WKHPTag.ENG_PRODUCTION_POOL03, 283 | WKHPTag.ENG_PRODUCTION_POOL04, 284 | WKHPTag.ENG_PRODUCTION_POOL05, 285 | WKHPTag.ENG_PRODUCTION_POOL06, 286 | WKHPTag.ENG_PRODUCTION_POOL07, 287 | WKHPTag.ENG_PRODUCTION_POOL08, 288 | WKHPTag.ENG_PRODUCTION_POOL09, 289 | WKHPTag.ENG_PRODUCTION_POOL10, 290 | WKHPTag.ENG_PRODUCTION_POOL11, 291 | WKHPTag.ENG_PRODUCTION_POOL12] 292 | resPool = await self._coordinator.async_read_values(tags) 293 | found = False 294 | for value in resPool: 295 | if resPool.get(value, {"value": "unknown"})["value"] == "unknown": 296 | found = True 297 | if len(resPool) == 12 and not found: 298 | break 299 | for x in range(2): 300 | tags = [WKHPTag.ENG_HEATPUMP_COP_MONTH01, 301 | WKHPTag.ENG_HEATPUMP_COP_MONTH02, 302 | WKHPTag.ENG_HEATPUMP_COP_MONTH03, 303 | WKHPTag.ENG_HEATPUMP_COP_MONTH04, 304 | WKHPTag.ENG_HEATPUMP_COP_MONTH05, 305 | WKHPTag.ENG_HEATPUMP_COP_MONTH06, 306 | WKHPTag.ENG_HEATPUMP_COP_MONTH07, 307 | WKHPTag.ENG_HEATPUMP_COP_MONTH08, 308 | WKHPTag.ENG_HEATPUMP_COP_MONTH09, 309 | WKHPTag.ENG_HEATPUMP_COP_MONTH10, 310 | WKHPTag.ENG_HEATPUMP_COP_MONTH11, 311 | WKHPTag.ENG_HEATPUMP_COP_MONTH12] 312 | resHeatpumpCopMonth = await self._coordinator.async_read_values(tags) 313 | found = False 314 | for value in resHeatpumpCopMonth: 315 | if resHeatpumpCopMonth.get(value, {"value": "unknown"})["value"] == "unknown": 316 | found = True 317 | if len(resHeatpumpCopMonth) == 12 and not found: 318 | break 319 | for x in range(2): 320 | tags = [WKHPTag.DATE_MONTH, 321 | WKHPTag.DATE_YEAR, 322 | WKHPTag.COP_HEATPUMP_YEAR, 323 | WKHPTag.COP_HEATPUMP_ACTUAL_YEAR_INFO] 324 | resDate = await self._coordinator.async_read_values(tags) 325 | found = False 326 | for value in resDate: 327 | if resDate.get(value, {"value": "unknown"})["value"] == "unknown": 328 | found = True 329 | if len(resDate) == 4 and not found: 330 | break 331 | except ValueError: 332 | return "unavailable" 333 | ret = { 334 | "cop_year": resDate.get(WKHPTag.COP_HEATPUMP_ACTUAL_YEAR_INFO, {"value": "unknown"})["value"], 335 | "cop": resDate.get(WKHPTag.COP_HEATPUMP_YEAR, {"value": "unknown"})["value"], 336 | "heatpump_month": resDate.get(WKHPTag.DATE_MONTH, {"value": "unknown"})["value"], 337 | "heatpump_year": resDate.get(WKHPTag.DATE_YEAR, {"value": "unknown"})["value"], 338 | "month_01": { 339 | "cop": resHeatpumpCopMonth.get(WKHPTag.ENG_HEATPUMP_COP_MONTH01, {"value": "unknown"})["value"], 340 | "compressor": resCompressor.get(WKHPTag.ENG_CONSUMPTION_COMPRESSOR01, {"value": "unknown"})["value"], 341 | "sourcepump": resSourcePump.get(WKHPTag.ENG_CONSUMPTION_SOURCEPUMP01, {"value": "unknown"})["value"], 342 | "externalheater": resExternalHeater.get(WKHPTag.ENG_CONSUMPTION_EXTERNALHEATER01, {"value": "unknown"})[ 343 | "value"], 344 | "heating": resHeater.get(WKHPTag.ENG_PRODUCTION_HEATING01, {"value": "unknown"})["value"], 345 | "warmwater": resWarmWater.get(WKHPTag.ENG_PRODUCTION_WARMWATER01, {"value": "unknown"})["value"], 346 | "pool": resPool.get(WKHPTag.ENG_PRODUCTION_POOL01, {"value": "unknown"})["value"] 347 | }, 348 | "month_02": { 349 | "cop": resHeatpumpCopMonth.get(WKHPTag.ENG_HEATPUMP_COP_MONTH02, {"value": "unknown"})["value"], 350 | "compressor": resCompressor.get(WKHPTag.ENG_CONSUMPTION_COMPRESSOR02, {"value": "unknown"})["value"], 351 | "sourcepump": resSourcePump.get(WKHPTag.ENG_CONSUMPTION_SOURCEPUMP02, {"value": "unknown"})["value"], 352 | "externalheater": resExternalHeater.get(WKHPTag.ENG_CONSUMPTION_EXTERNALHEATER02, {"value": "unknown"})[ 353 | "value"], 354 | "heating": resHeater.get(WKHPTag.ENG_PRODUCTION_HEATING02, {"value": "unknown"})["value"], 355 | "warmwater": resWarmWater.get(WKHPTag.ENG_PRODUCTION_WARMWATER02, {"value": "unknown"})["value"], 356 | "pool": resPool.get(WKHPTag.ENG_PRODUCTION_POOL02, {"value": "unknown"})["value"] 357 | }, 358 | "month_03": { 359 | "cop": resHeatpumpCopMonth.get(WKHPTag.ENG_HEATPUMP_COP_MONTH03, {"value": "unknown"})["value"], 360 | "compressor": resCompressor.get(WKHPTag.ENG_CONSUMPTION_COMPRESSOR03, {"value": "unknown"})["value"], 361 | "sourcepump": resSourcePump.get(WKHPTag.ENG_CONSUMPTION_SOURCEPUMP03, {"value": "unknown"})["value"], 362 | "externalheater": resExternalHeater.get(WKHPTag.ENG_CONSUMPTION_EXTERNALHEATER03, {"value": "unknown"})[ 363 | "value"], 364 | "heating": resHeater.get(WKHPTag.ENG_PRODUCTION_HEATING03, {"value": "unknown"})["value"], 365 | "warmwater": resWarmWater.get(WKHPTag.ENG_PRODUCTION_WARMWATER03, {"value": "unknown"})["value"], 366 | "pool": resPool.get(WKHPTag.ENG_PRODUCTION_POOL03, {"value": "unknown"})["value"] 367 | }, 368 | "month_04": { 369 | "cop": resHeatpumpCopMonth.get(WKHPTag.ENG_HEATPUMP_COP_MONTH04, {"value": "unknown"})["value"], 370 | "compressor": resCompressor.get(WKHPTag.ENG_CONSUMPTION_COMPRESSOR04, {"value": "unknown"})["value"], 371 | "sourcepump": resSourcePump.get(WKHPTag.ENG_CONSUMPTION_SOURCEPUMP04, {"value": "unknown"})["value"], 372 | "externalheater": resExternalHeater.get(WKHPTag.ENG_CONSUMPTION_EXTERNALHEATER04, {"value": "unknown"})[ 373 | "value"], 374 | "heating": resHeater.get(WKHPTag.ENG_PRODUCTION_HEATING04, {"value": "unknown"})["value"], 375 | "warmwater": resWarmWater.get(WKHPTag.ENG_PRODUCTION_WARMWATER04, {"value": "unknown"})["value"], 376 | "pool": resPool.get(WKHPTag.ENG_PRODUCTION_POOL04, {"value": "unknown"})["value"] 377 | }, 378 | "month_05": { 379 | "cop": resHeatpumpCopMonth.get(WKHPTag.ENG_HEATPUMP_COP_MONTH05, {"value": "unknown"})["value"], 380 | "compressor": resCompressor.get(WKHPTag.ENG_CONSUMPTION_COMPRESSOR05, {"value": "unknown"})["value"], 381 | "sourcepump": resSourcePump.get(WKHPTag.ENG_CONSUMPTION_SOURCEPUMP05, {"value": "unknown"})["value"], 382 | "externalheater": resExternalHeater.get(WKHPTag.ENG_CONSUMPTION_EXTERNALHEATER05, {"value": "unknown"})[ 383 | "value"], 384 | "heating": resHeater.get(WKHPTag.ENG_PRODUCTION_HEATING05, {"value": "unknown"})["value"], 385 | "warmwater": resWarmWater.get(WKHPTag.ENG_PRODUCTION_WARMWATER05, {"value": "unknown"})["value"], 386 | "pool": resPool.get(WKHPTag.ENG_PRODUCTION_POOL05, {"value": "unknown"})["value"] 387 | }, 388 | "month_06": { 389 | "cop": resHeatpumpCopMonth.get(WKHPTag.ENG_HEATPUMP_COP_MONTH06, {"value": "unknown"})["value"], 390 | "compressor": resCompressor.get(WKHPTag.ENG_CONSUMPTION_COMPRESSOR06, {"value": "unknown"})["value"], 391 | "sourcepump": resSourcePump.get(WKHPTag.ENG_CONSUMPTION_SOURCEPUMP06, {"value": "unknown"})["value"], 392 | "externalheater": resExternalHeater.get(WKHPTag.ENG_CONSUMPTION_EXTERNALHEATER06, {"value": "unknown"})[ 393 | "value"], 394 | "heating": resHeater.get(WKHPTag.ENG_PRODUCTION_HEATING06, {"value": "unknown"})["value"], 395 | "warmwater": resWarmWater.get(WKHPTag.ENG_PRODUCTION_WARMWATER06, {"value": "unknown"})["value"], 396 | "pool": resPool.get(WKHPTag.ENG_PRODUCTION_POOL06, {"value": "unknown"})["value"] 397 | }, 398 | "month_07": { 399 | "cop": resHeatpumpCopMonth.get(WKHPTag.ENG_HEATPUMP_COP_MONTH07, {"value": "unknown"})["value"], 400 | "compressor": resCompressor.get(WKHPTag.ENG_CONSUMPTION_COMPRESSOR07, {"value": "unknown"})["value"], 401 | "sourcepump": resSourcePump.get(WKHPTag.ENG_CONSUMPTION_SOURCEPUMP07, {"value": "unknown"})["value"], 402 | "externalheater": resExternalHeater.get(WKHPTag.ENG_CONSUMPTION_EXTERNALHEATER07, {"value": "unknown"})[ 403 | "value"], 404 | "heating": resHeater.get(WKHPTag.ENG_PRODUCTION_HEATING07, {"value": "unknown"})["value"], 405 | "warmwater": resWarmWater.get(WKHPTag.ENG_PRODUCTION_WARMWATER07, {"value": "unknown"})["value"], 406 | "pool": resPool.get(WKHPTag.ENG_PRODUCTION_POOL07, {"value": "unknown"})["value"] 407 | }, 408 | "month_08": { 409 | "cop": resHeatpumpCopMonth.get(WKHPTag.ENG_HEATPUMP_COP_MONTH08, {"value": "unknown"})["value"], 410 | "compressor": resCompressor.get(WKHPTag.ENG_CONSUMPTION_COMPRESSOR08, {"value": "unknown"})["value"], 411 | "sourcepump": resSourcePump.get(WKHPTag.ENG_CONSUMPTION_SOURCEPUMP08, {"value": "unknown"})["value"], 412 | "externalheater": resExternalHeater.get(WKHPTag.ENG_CONSUMPTION_EXTERNALHEATER08, {"value": "unknown"})[ 413 | "value"], 414 | "heating": resHeater.get(WKHPTag.ENG_PRODUCTION_HEATING08, {"value": "unknown"})["value"], 415 | "warmwater": resWarmWater.get(WKHPTag.ENG_PRODUCTION_WARMWATER08, {"value": "unknown"})["value"], 416 | "pool": resPool.get(WKHPTag.ENG_PRODUCTION_POOL08, {"value": "unknown"})["value"] 417 | }, 418 | "month_09": { 419 | "cop": resHeatpumpCopMonth.get(WKHPTag.ENG_HEATPUMP_COP_MONTH09, {"value": "unknown"})["value"], 420 | "compressor": resCompressor.get(WKHPTag.ENG_CONSUMPTION_COMPRESSOR09, {"value": "unknown"})["value"], 421 | "sourcepump": resSourcePump.get(WKHPTag.ENG_CONSUMPTION_SOURCEPUMP09, {"value": "unknown"})["value"], 422 | "externalheater": resExternalHeater.get(WKHPTag.ENG_CONSUMPTION_EXTERNALHEATER09, {"value": "unknown"})[ 423 | "value"], 424 | "heating": resHeater.get(WKHPTag.ENG_PRODUCTION_HEATING09, {"value": "unknown"})["value"], 425 | "warmwater": resWarmWater.get(WKHPTag.ENG_PRODUCTION_WARMWATER09, {"value": "unknown"})["value"], 426 | "pool": resPool.get(WKHPTag.ENG_PRODUCTION_POOL09, {"value": "unknown"})["value"] 427 | }, 428 | "month_10": { 429 | "cop": resHeatpumpCopMonth.get(WKHPTag.ENG_HEATPUMP_COP_MONTH10, {"value": "unknown"})["value"], 430 | "compressor": resCompressor.get(WKHPTag.ENG_CONSUMPTION_COMPRESSOR10, {"value": "unknown"})["value"], 431 | "sourcepump": resSourcePump.get(WKHPTag.ENG_CONSUMPTION_SOURCEPUMP10, {"value": "unknown"})["value"], 432 | "externalheater": resExternalHeater.get(WKHPTag.ENG_CONSUMPTION_EXTERNALHEATER10, {"value": "unknown"})[ 433 | "value"], 434 | "heating": resHeater.get(WKHPTag.ENG_PRODUCTION_HEATING10, {"value": "unknown"})["value"], 435 | "warmwater": resWarmWater.get(WKHPTag.ENG_PRODUCTION_WARMWATER10, {"value": "unknown"})["value"], 436 | "pool": resPool.get(WKHPTag.ENG_PRODUCTION_POOL10, {"value": "unknown"})["value"] 437 | }, 438 | "month_11": { 439 | "cop": resHeatpumpCopMonth.get(WKHPTag.ENG_HEATPUMP_COP_MONTH11, {"value": "unknown"})["value"], 440 | "compressor": resCompressor.get(WKHPTag.ENG_CONSUMPTION_COMPRESSOR11, {"value": "unknown"})["value"], 441 | "sourcepump": resSourcePump.get(WKHPTag.ENG_CONSUMPTION_SOURCEPUMP11, {"value": "unknown"})["value"], 442 | "externalheater": resExternalHeater.get(WKHPTag.ENG_CONSUMPTION_EXTERNALHEATER11, {"value": "unknown"})[ 443 | "value"], 444 | "heating": resHeater.get(WKHPTag.ENG_PRODUCTION_HEATING11, {"value": "unknown"})["value"], 445 | "warmwater": resWarmWater.get(WKHPTag.ENG_PRODUCTION_WARMWATER11, {"value": "unknown"})["value"], 446 | "pool": resPool.get(WKHPTag.ENG_PRODUCTION_POOL11, {"value": "unknown"})["value"] 447 | }, 448 | "month_12": { 449 | "cop": resHeatpumpCopMonth.get(WKHPTag.ENG_HEATPUMP_COP_MONTH12, {"value": "unknown"})["value"], 450 | "compressor": resCompressor.get(WKHPTag.ENG_CONSUMPTION_COMPRESSOR12, {"value": "unknown"})["value"], 451 | "sourcepump": resSourcePump.get(WKHPTag.ENG_CONSUMPTION_SOURCEPUMP12, {"value": "unknown"})["value"], 452 | "externalheater": resExternalHeater.get(WKHPTag.ENG_CONSUMPTION_EXTERNALHEATER12, {"value": "unknown"})[ 453 | "value"], 454 | "heating": resHeater.get(WKHPTag.ENG_PRODUCTION_HEATING12, {"value": "unknown"})["value"], 455 | "warmwater": resWarmWater.get(WKHPTag.ENG_PRODUCTION_WARMWATER12, {"value": "unknown"})["value"], 456 | "pool": resPool.get(WKHPTag.ENG_PRODUCTION_POOL12, {"value": "unknown"})["value"] 457 | } 458 | } 459 | return ret 460 | -------------------------------------------------------------------------------- /custom_components/waterkotte_heatpump/pywaterkotte_ha/__init__.py: -------------------------------------------------------------------------------- 1 | import asyncio 2 | import logging 3 | import re 4 | import xml.etree.ElementTree as ElemTree 5 | from datetime import datetime 6 | from typing import ( 7 | Any, 8 | Sequence, 9 | Tuple, 10 | List, 11 | Collection 12 | ) 13 | 14 | from custom_components.waterkotte_heatpump.pywaterkotte_ha.const import ( 15 | ECOTOUCH, 16 | EASYCON, 17 | SERIES, 18 | SYSTEM_IDS, 19 | SIX_STEPS_MODES, 20 | TRANSLATIONS 21 | ) 22 | from custom_components.waterkotte_heatpump.pywaterkotte_ha.error import ( 23 | InvalidResponseException, 24 | InvalidValueException, 25 | StatusException, 26 | TooManyUsersException, 27 | Http404Exception, InvalidPasswordException 28 | ) 29 | from custom_components.waterkotte_heatpump.pywaterkotte_ha.tags import WKHPTag 30 | 31 | _LOGGER: logging.Logger = logging.getLogger(__package__) 32 | 33 | 34 | class WaterkotteClient: 35 | def __init__(self, host: str, username: str, pwd: str, system_type: str, web_session, 36 | tags: list, tags_per_request: int, lang: str = "en") -> None: 37 | self._host = host 38 | self._systemType = system_type 39 | if system_type == ECOTOUCH: 40 | self._internal_client = EcotouchBridge(host=host, web_session=web_session, username=username, 41 | pwd=pwd, tags_per_request=tags_per_request, lang=lang) 42 | elif system_type == EASYCON: 43 | self._internal_client = EasyconBridge(host=host, web_session=web_session) 44 | else: 45 | _LOGGER.error("Error unknown System type!") 46 | 47 | self.tags = tags 48 | 49 | @property 50 | def tags(self): 51 | """getter for Tags""" 52 | return self.__tags 53 | 54 | @tags.setter 55 | def tags(self, tags): 56 | if tags is not None: 57 | _LOGGER.info(f"number of tags to query set to: {len(tags)}") 58 | self.__tags = tags 59 | 60 | async def login(self) -> None: 61 | if self._internal_client.auth_cookies is None: 62 | try: 63 | await self._internal_client.login() 64 | 65 | except TooManyUsersException: 66 | _LOGGER.warning(f"TooManyUsers while try to login - will just sleep 30sec") 67 | await asyncio.sleep(30) 68 | 69 | except Exception as exc: # pylint: disable=broad-except 70 | _LOGGER.error(f"Error while login will retry in 15sec: {exc}") 71 | await asyncio.sleep(15) 72 | await self._internal_client.logout() 73 | try: 74 | await self._internal_client.login() 75 | except Exception as exc2: 76 | _LOGGER.error(f"Error while RETRY login: {exc2}") 77 | 78 | async def logout(self) -> None: 79 | await self._internal_client.logout() 80 | 81 | async def async_get_data(self) -> dict: 82 | if self.tags is not None: 83 | res = await self._internal_client.read_values(self.tags) 84 | return res 85 | 86 | async def async_read_values(self, tags: Sequence[WKHPTag]) -> dict: 87 | res = await self._internal_client.read_values(tags) 88 | return res 89 | 90 | async def async_read_value(self, tag: WKHPTag) -> dict: 91 | res = await self._internal_client.read_value(tag) 92 | return res 93 | 94 | async def async_write_value(self, tag: WKHPTag, value): 95 | res = await self._internal_client.write_value(tag, value) 96 | return res 97 | 98 | async def async_write_values(self, kv_pairs: Collection[Tuple[WKHPTag, Any]]): 99 | res = await self._internal_client.write_values(kv_pairs) 100 | return res 101 | 102 | 103 | # 104 | class EcotouchBridge: 105 | auth_cookies = None 106 | 107 | def __init__(self, host: str, web_session, username: str ="waterkotte", pwd: str = "waterkotte", tags_per_request: int = 10, lang: str = "en"): 108 | self.host = host 109 | self.username = username 110 | self.pwd = pwd 111 | self.web_session = web_session 112 | self.tags_per_request = min(tags_per_request, 75) 113 | self.lang_map = None 114 | if lang in TRANSLATIONS: 115 | self.lang_map = TRANSLATIONS[lang] 116 | else: 117 | self.lang_map = TRANSLATIONS["en"] 118 | 119 | # def set_token(self, token:str): 120 | # cookies: SimpleCookie[str] = SimpleCookie() 121 | # cookies.load(f"Set-Cookie: IDALToken={token}; Path=/") 122 | # if hasattr(self.web_session, "_cookie_jar"): 123 | # jar = getattr(self.web_session, "_cookie_jar") 124 | # jar.update_cookies(cookies) 125 | # self.auth_cookies = cookies 126 | 127 | # extracts statuscode from response 128 | def get_status_response(self, r): # pylint: disable=invalid-name 129 | """get_status_response""" 130 | match = re.search(r"^#([A-Z_]+)", r, re.MULTILINE) 131 | if match is None: 132 | raise InvalidResponseException("Invalid reply. Status could not be parsed") 133 | return match.group(1) 134 | 135 | # performs a login. Has to be called before any other method. 136 | async def login(self): 137 | """Login to Heat Pump""" 138 | _LOGGER.info(f"login to waterkotte host {self.host}") 139 | 140 | # it's only possible to adjust the password of the 'waterkotte' build in user 141 | args = {"username": self.username, "password": self.pwd} 142 | 143 | async with self.web_session.get(f"http://{self.host}/cgi/login", params=args) as response: 144 | response.raise_for_status() 145 | if response.status == 200: 146 | content = await response.text() 147 | 148 | tc = content.replace('\n', '') 149 | tc = tc.replace('\r', '') 150 | _LOGGER.info(f"LOGIN status:{response.status} response: {tc}") 151 | 152 | parsed_response = self.get_status_response(content) 153 | if parsed_response != "S_OK": 154 | if parsed_response.startswith("E_TOO_MANY_USERS"): 155 | raise TooManyUsersException("TOO_MANY_USERS") 156 | elif parsed_response.startswith("E_PASS_DONT_MATCH"): 157 | raise InvalidPasswordException("INVALID_PWD") 158 | else: 159 | raise StatusException(f"Error while LOGIN: status: {parsed_response}") 160 | 161 | # since this is a get, we have to do our own cookie handling... 162 | if response.cookies is not None: 163 | self.auth_cookies = response.cookies 164 | _LOGGER.debug(f"{self.auth_cookies}") 165 | if hasattr(self.web_session, "_cookie_jar"): 166 | jar = getattr(self.web_session, "_cookie_jar") 167 | jar.update_cookies(response.cookies) 168 | 169 | else: 170 | _LOGGER.warning(f"{response}") 171 | 172 | async def logout(self): 173 | """Logout function""" 174 | async with self.web_session.get(f"http://{self.host}/cgi/logout") as response: 175 | try: 176 | response.raise_for_status() 177 | content = await response.text() 178 | _LOGGER.info(f"LOGOUT status:{response.status} content: {content}") 179 | except Exception as exc: 180 | _LOGGER.warning(f"{exc}") 181 | 182 | self.auth_cookies = None 183 | 184 | async def read_value(self, tag: WKHPTag): 185 | """Read a value from Tag""" 186 | res = await self.read_values([tag]) 187 | if tag in res: 188 | return res[tag] 189 | return None 190 | 191 | async def read_values(self, tags: Sequence[WKHPTag]): 192 | if self.auth_cookies is None: 193 | await self.login() 194 | 195 | """Async read values""" 196 | # create flat list of ecotouch tags to be read 197 | e_tags = list(set([etag for tag in tags for etag in tag.tags])) 198 | e_values, e_status = await self._read_tags(e_tags) 199 | 200 | result = {} 201 | if e_values is not None and len(e_values) > 0: 202 | for a_wphp_tag in tags: 203 | try: 204 | t_values = [e_values[a_tag] for a_tag in a_wphp_tag.tags] 205 | t_states = [e_status[a_tag] for a_tag in a_wphp_tag.tags] 206 | 207 | if t_values is None or (len(t_values) > 0 and t_values[0] is None): 208 | if t_states is not None and len(t_states)>0: 209 | result[a_wphp_tag] = { 210 | "value": None, 211 | "status": t_states[0] 212 | } 213 | else: 214 | result[a_wphp_tag] = None 215 | else: 216 | if a_wphp_tag.decode_f == WKHPTag._decode_alarms: 217 | result[a_wphp_tag] = { 218 | "value": a_wphp_tag.decode_f(a_wphp_tag, t_values, self.lang_map), 219 | "status": t_states[0] 220 | } 221 | else: 222 | result[a_wphp_tag] = { 223 | "value": a_wphp_tag.decode_f(a_wphp_tag, t_values), 224 | "status": t_states[0] 225 | } 226 | 227 | if a_wphp_tag.translate and a_wphp_tag.tags[0] in self.lang_map: 228 | value_map = self.lang_map[a_wphp_tag.tags[0]] 229 | final_value = "" 230 | temp_values = result[a_wphp_tag]["value"] 231 | if temp_values is not None: 232 | for idx in range(len(temp_values)): 233 | if temp_values[idx]: 234 | final_value = final_value + ", " + str(value_map[idx]) 235 | 236 | # we need to trim the firsts initial added ', ' 237 | if len(final_value) > 0: 238 | final_value = final_value[2:] 239 | 240 | result[a_wphp_tag]["value"] = final_value 241 | 242 | except KeyError: 243 | _LOGGER.warning( 244 | f"Key Error while read_values. EcoTag: {a_wphp_tag} t_values: {t_values} t_states: {t_states}") 245 | except Exception as other_exc: 246 | _LOGGER.error( 247 | f"Exception of type '{other_exc}' while read_values. EcoTag: {a_wphp_tag} t_values: {t_values} t_states: {t_states} -> {other_exc}" 248 | ) 249 | 250 | return result 251 | 252 | async def _read_tags(self, tags: Sequence[WKHPTag], results=None, results_status=None): 253 | if results is None: 254 | results = {} 255 | if results_status is None: 256 | results_status = {} 257 | 258 | max_read_tags = self.tags_per_request 259 | while len(tags) > max_read_tags: 260 | results, results_status = await self._read_tags(tags[:max_read_tags], results, results_status) 261 | tags = tags[max_read_tags:] 262 | 263 | args = {} 264 | args["n"] = len(tags) 265 | for i in range(len(tags)): 266 | args[f"t{(i + 1)}"] = tags[i] 267 | 268 | # also the readTags have a timestamp in each request... 269 | args["_"] = str(int(round(datetime.now().timestamp() * 1000))) 270 | _LOGGER.info(f"going to request {args['n']} tags in a single call from waterkotte@{self.host}") 271 | async with self.web_session.get(f"http://{self.host}/cgi/readTags", params=args) as response: 272 | try: 273 | response.raise_for_status() 274 | if response.status == 200: 275 | _LOGGER.debug(f"requested: {response.url}") 276 | content = await response.text() 277 | 278 | # faking READING 3:HREG values... [DEBUG ONLY] 279 | # content = content.replace('\n4\t', '\n192\t52') 280 | 281 | if content.startswith("#E_NEED_LOGIN"): 282 | try: 283 | await self.login() 284 | return await self._read_tags(tags=tags, results=results, results_status=results_status) 285 | except StatusException as status_exec: 286 | _LOGGER.warning(f"StatusException (_read_tags) while trying to login: {status_exec}") 287 | return None, None 288 | 289 | if content.startswith("#E_TOO_MANY_USERS"): 290 | return None 291 | 292 | for tag in tags: 293 | match = re.search( 294 | # rf"#{tag}\t(?P[A-Z_]+)\n\d+\t(?P\-?\d+)", 295 | rf"#{tag}\t(?P[A-Z_]+)\n(?P\d+)\t(?P[-+]?(?:\d*\.?\d+))", 296 | content, 297 | re.MULTILINE, 298 | ) 299 | if match is None: 300 | match = re.search( 301 | rf"#{tag}\t(?P[A-Z_]+)\n(?P\d+)\t", 302 | content, 303 | re.MULTILINE, 304 | ) 305 | if match is None: 306 | # ok let's check for INACTIVETAG... 307 | match = re.search( 308 | rf"#{tag}\tE_INACTIVETAG", 309 | content, 310 | re.MULTILINE, 311 | ) 312 | if match is None: 313 | # special handling for "unknown" tags in the ALARM_BITS field... [if one of the 314 | # I2xxx Tags is not known, we're simply going to remove that tag from the tag list] 315 | if tag in WKHPTag.ALARM_BITS.tags: 316 | WKHPTag.ALARM_BITS.tags.remove(tag) 317 | _LOGGER.info(f"Tag: '{tag}' not found in response - removing tag from WKHPTag.ALARM_BITS") 318 | else: 319 | _LOGGER.warning(f"Tag: '{tag}' not found in response!") 320 | results_status[tag] = "E_NOTFOUND" 321 | else: 322 | results_status[tag] = "E_INACTIVE" 323 | else: 324 | _LOGGER.warning(f"Tag: '{tag}' without value! -> opt-code: {match.group('opt')}") 325 | results_status[tag] = match.group("status") 326 | 327 | results[tag] = None 328 | else: 329 | results_status[tag] = match.group("status") 330 | results[tag] = match.group("value") 331 | 332 | else: 333 | _LOGGER.warning(f"{response}") 334 | except Exception as exc: 335 | if response is not None and response.status == 500: 336 | self.auth_cookies = None 337 | await self.login() 338 | return await self._read_tags(tags) 339 | else: 340 | _LOGGER.warning(f"{exc}") 341 | 342 | return results, results_status 343 | 344 | async def write_value(self, tag, value): 345 | """Write a value""" 346 | return await self.write_values([(tag, value)]) 347 | 348 | async def write_values(self, kv_pairs: Collection[Tuple[WKHPTag, Any]]): 349 | if self.auth_cookies is None: 350 | await self.login() 351 | 352 | """Write values to Tag""" 353 | to_write = {} 354 | result = {} 355 | 356 | # we write only one WKHPTag at the same time (but the WKHPTag can consist of 357 | # multiple internal tag fields) 358 | for a_wkhp_tag, value in kv_pairs: # pylint: disable=invalid-name 359 | if not a_wkhp_tag.writeable: 360 | raise InvalidValueException("tried to write to an readonly field") 361 | # converting the HA values to the final int or bools that the waterkotte understand 362 | a_wkhp_tag.encode_f(a_wkhp_tag, value, to_write) 363 | 364 | _LOGGER.info(f"before writing WKHPTags -> {len(to_write)} tags") 365 | # '.keys()' doesn't support insertion - so we need to create a new list object! 366 | e_values, e_status = await self._write_tags(tags=list(to_write.keys()), values=list(to_write.values())) 367 | 368 | if e_values is not None and len(e_values) > 0: 369 | _LOGGER.info(f"after writing WKHPTags -> raw-values: {len(e_values)} states: {len(e_status)}") 370 | 371 | all_ok = True 372 | for a_tag in e_status: 373 | if e_status[a_tag] != "S_OK": 374 | all_ok = False 375 | 376 | if all_ok: 377 | str_vals = [e_values[a_tag] for a_tag in a_wkhp_tag.tags] 378 | val = a_wkhp_tag.decode_f(a_wkhp_tag, str_vals) 379 | 380 | # special 24:00:00 time handling 381 | if str(value).startswith("23:59:59.9"): 382 | value = "00:00:00" 383 | 384 | if str(val) != str(value): 385 | _LOGGER.error(f"WRITE value does not match READ value: '{val}' (read) != '{value}' (write)") 386 | else: 387 | result[a_wkhp_tag] = { 388 | "value": val, 389 | # here we also take just the first status... 390 | "status": e_status[a_wkhp_tag.tags[0]] 391 | } 392 | return result 393 | 394 | async def _write_tags(self, tags: list[str], values: list[Any], results=None, results_status=None): 395 | if results is None: 396 | results = {} 397 | if results_status is None: 398 | results_status = {} 399 | 400 | max_write_tags = self.tags_per_request 401 | while len(tags) > max_write_tags: 402 | results, results_status = await self._write_tags( 403 | tags=tags[:max_write_tags], values=values[:max_write_tags], 404 | results=results, results_status=results_status 405 | ) 406 | tags = tags[max_write_tags:] 407 | values = values[max_write_tags:] 408 | 409 | args = {} 410 | args["n"] = len(tags) 411 | args["returnValue"] = "true" 412 | args["rnd"] = str(int(round(datetime.now().timestamp() * 1000))) 413 | for i, tag in enumerate(tags): 414 | args[f"t{i + 1}"] = tag 415 | args[f"v{i + 1}"] = list(values)[i] 416 | 417 | _LOGGER.info(f"going to request {args['n']} tags in a single call from waterkotte@{self.host}") 418 | async with self.web_session.get(f"http://{self.host}/cgi/writeTags", params=args) as response: 419 | try: 420 | response.raise_for_status() 421 | if response.status == 200: 422 | _LOGGER.debug(f"requested: {response.url}") 423 | content = await response.text() # pylint: disable=invalid-name 424 | if content.startswith("#E_NEED_LOGIN"): 425 | try: 426 | await self.login() 427 | return await self._write_tags(tags=tags, values=values) 428 | except StatusException as status_exec: 429 | _LOGGER.warning(f"StatusException (_write_tags) while trying to login: {status_exec}") 430 | return None 431 | if content.startswith("#E_TOO_MANY_USERS"): 432 | return None 433 | 434 | ### 435 | for tag in tags: 436 | match = re.search( 437 | # rf"#{tag}\t(?P[A-Z_]+)\n\d+\t(?P\-?\d+)", 438 | rf"#{tag}\t(?P[A-Z_]+)\n(?P\d+)\t(?P[-+]?(?:\d*\.?\d+))", 439 | content, 440 | re.MULTILINE 441 | ) 442 | if match is None: 443 | match = re.search( 444 | rf"#{tag}\t(?P[A-Z_]+)\n(?P\d+)\t", 445 | content, 446 | re.MULTILINE, 447 | ) 448 | if match is None: 449 | # ok let's check for INACTIVETAG... 450 | match = re.search( 451 | rf"#{tag}\tE_INACTIVETAG", 452 | content, 453 | re.MULTILINE, 454 | ) 455 | if match is None: 456 | _LOGGER.warning(f"Tag: '{tag}' not found in response!") 457 | results_status[tag] = "E_NOTFOUND" 458 | else: 459 | results_status[tag] = "E_INACTIVE" 460 | else: 461 | _LOGGER.warning(f"Tag: '{tag}' without value! -> opt-code: {match.group('opt')}") 462 | results_status[tag] = match.group("status") 463 | 464 | results[tag] = None 465 | else: 466 | results_status[tag] = match.group("status") 467 | results[tag] = match.group("value") 468 | 469 | else: 470 | _LOGGER.warning(f"{response}") 471 | except Exception as exc: 472 | _LOGGER.warning(f"{exc}") 473 | 474 | return results, results_status 475 | 476 | 477 | class EasyconBridge(EcotouchBridge): 478 | """Base Easycon Class, inherits from ecotouch""" 479 | 480 | async def login(self): # pylint: disable=unused-argument 481 | """Login to Heat Pump (not needed for easycon)""" 482 | return 483 | 484 | async def logout(self): 485 | """Logout function (not needed for easycon)""" 486 | return 487 | 488 | # reads a list of ecotouch tags 489 | # 490 | async def _read_tags(self, tags: Sequence[WKHPTag], results=None, results_status=None): 491 | """async read tags""" 492 | if results is None: 493 | results = {} 494 | if results_status is None: 495 | results_status = {} 496 | D = [] # pylint: disable=invalid-name 497 | I = [] # pylint: disable=invalid-name 498 | A = [] # pylint: disable=invalid-name 499 | for tag in tags: 500 | print(tag) 501 | # for entry in tag.tags: 502 | # print(entry) 503 | if tag[0] == "D": 504 | D.append(int(tag[1:])) 505 | elif tag[0] == "I": 506 | I.append(int(tag[1:])) 507 | elif tag[0] == "A": 508 | A.append(int(tag[1:])) 509 | 510 | D.sort() 511 | I.sort() 512 | A.sort() 513 | 514 | query = "" 515 | if len(D) > 0: 516 | query += f"|D|{D[0]}|{D[len(D) - 1]}" 517 | if len(A) > 0: 518 | query += f"|A|{A[0]}|{A[len(A) - 1]}" 519 | if len(I) > 0: 520 | query += f"|I|{I[0]}|{I[len(I) - 1]}" 521 | # query="?" + query[1:] 522 | print(query) 523 | if query == "": 524 | return None, None 525 | 526 | async with self.web_session.get(f"http://{self.host}/config/xml.cgi?{query[1:]}") as response: 527 | try: 528 | response.raise_for_status() 529 | if response.status == 200: 530 | try: 531 | content = await response.text() # pylint: disable=invalid-name 532 | tree = ElemTree.fromstring(content) 533 | root = tree[0] 534 | 535 | for tagType in root: 536 | for tag in tagType: 537 | if int(tag[0].text) < 50: 538 | print(f"{tagType.tag[0]}{tag[0].text}={tag[1].text}") 539 | 540 | # return None, None 541 | except Exception as exc: 542 | _LOGGER.debug(f"Response was: {content} caused {exc}") 543 | raise Exception(f"Error in easycon.py parsing. Received: {content}") 544 | 545 | for tag in tags: 546 | if tag[0] == "D": 547 | valType = "DIGITAL" 548 | elif tag[0] == "I": 549 | valType = "INTEGER" 550 | elif tag[0] == "A": 551 | valType = "ANALOG" 552 | match = root.find(f".//{valType}/*/INDEX[.='{tag[1:]}']/../VALUE") 553 | if match is None: 554 | match = re.search( 555 | # r"#%s\tE_INACTIVETAG" % tag, 556 | f"#{tag}\tE_INACTIVETAG", 557 | content, 558 | re.MULTILINE, 559 | ) 560 | # val_status = "E_INACTIVE" # pylint: disable=possibly-unused-variable 561 | # print("Tag: %s is inactive!", tag) 562 | if match is None: 563 | # special handling for "unknown" tags in the ALARM_BITS field... [if one of the 564 | # I2xxx Tags is not known, we're simply going to remove that tag from the tag list] 565 | if tag in WKHPTag.ALARM_BITS.tags: 566 | WKHPTag.ALARM_BITS.tags.remove(tag) 567 | _LOGGER.info(f"Tag: '{tag}' not found in response - removing tag from WKHPTag.ALARM_BITS") 568 | else: 569 | _LOGGER.warning(f"Tag: '{tag}' not found in response!") 570 | results_status[tag] = "E_NOTFOUND" 571 | else: 572 | # if val_status == "E_INACTIVE": 573 | results_status[tag] = "E_INACTIVE" 574 | 575 | results[tag] = None 576 | else: 577 | results_status[tag] = "S_OK" 578 | if valType == "ANALOG": 579 | results[tag] = str(float(match.text) * 10.0) 580 | else: 581 | results[tag] = match.text 582 | if response.status == 404: 583 | _LOGGER.debug(f"http 404 caused by requesting {response.url} - full: {response}") 584 | raise Http404Exception(f"HTTP 404 {response.url}") 585 | else: 586 | _LOGGER.warning(f"{response}") 587 | except Exception as exc: 588 | if response is not None and response.status == 404: 589 | _LOGGER.debug(f"http 404 caused by requesting {response.url} - full: {response}") 590 | raise Http404Exception(f"HTTP 404 {response.url}") 591 | else: 592 | _LOGGER.warning(f"{exc}") 593 | 594 | return results, results_status 595 | 596 | async def _write_tags(self, tags: list[str], values: list[Any], results=None, results_status=None): 597 | """write tag""" 598 | # for i in range(len(tags)): 599 | # args[f"t{(i + 1)}"] = tags[i] 600 | # for i in range(len(tag.tags)): 601 | # et_values[tag.tags[i]] = vals[i] 602 | # print(et_values) 603 | # http://192.168.0.193/config/query.cgi?var%7CI%7C1255%7C20%7Cvar%7CI%7C1256%7C01%7Cvar%7CI%7C1257%7C31%7Cvar%7CI%7C1258%7C01%7Cvar%7CI%7C1259%7C23%7C 604 | # var|I|1255|20|var|I|1256|01|var|I|1257|31|var|I|1258|01|var|I|1259|23| 605 | param = "" 606 | for i, tag in enumerate(tags): 607 | param += f"var|{tag[0].upper()}|{tag[1:]}|{list(values)[i]}|" 608 | 609 | results = {} 610 | resultsStatus = {} 611 | 612 | async with self.web_session.get(f"http://{self.host}/config/query.cgi?{param}") as response: 613 | try: 614 | response.raise_for_status() 615 | if response.status == 200: 616 | content = await response.text() 617 | 618 | is_ok = content.find("Operation completed succesfully") > 0 or content.find( 619 | "Operation completed successfully") > 0 620 | if is_ok and response.status == 200: 621 | 622 | for i, tag in enumerate(tags): 623 | resultsStatus[tag] = "S_OK" 624 | results[tag] = list(values)[i] 625 | else: 626 | _LOGGER.warning(f"{response}") 627 | except Exception as exc: 628 | _LOGGER.warning(f"{exc}") 629 | 630 | return results, resultsStatus 631 | --------------------------------------------------------------------------------