├── 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 | 
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 | [](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 | [](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 | [](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 |
--------------------------------------------------------------------------------