python-pooldose 0.3.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
pooldose/__init__.py ADDED
@@ -0,0 +1,4 @@
1
+ """Async API client for SEKO Pooldose."""
2
+ from .client import PooldoseClient
3
+
4
+ __all__ = ["PooldoseClient"]
pooldose/client.py ADDED
@@ -0,0 +1,228 @@
1
+ """Client for async API client for SEKO Pooldose."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import asyncio
6
+ import logging
7
+ from typing import Optional
8
+ from pooldose.values.instant_values import InstantValues
9
+ from pooldose.request_handler import RequestHandler, RequestStatus
10
+ from pooldose.values.static_values import StaticValues
11
+ from pooldose.mappings.mapping_info import (
12
+ MappingInfo,
13
+ SensorMapping,
14
+ BinarySensorMapping,
15
+ NumberMapping,
16
+ SwitchMapping,
17
+ SelectMapping,
18
+ )
19
+
20
+ # pylint: disable=line-too-long
21
+
22
+ _LOGGER = logging.getLogger(__name__)
23
+
24
+ class PooldoseClient:
25
+ """
26
+ Async client for SEKO Pooldose API.
27
+ All getter methods return (status, data) and log errors.
28
+ """
29
+
30
+ def __init__(self, host: str, timeout: int = 10) -> None:
31
+ """
32
+ Initialize the Pooldose client.
33
+
34
+ Args:
35
+ host (str): The host address of the Pooldose device.
36
+ timeout (int): Timeout for API requests in seconds.
37
+ """
38
+ self._host = host
39
+ self._timeout = timeout
40
+ self._last_data = None
41
+ self._request_handler = None
42
+
43
+ # Initialize device info with default or placeholder values
44
+ self.device_info = {
45
+ "NAME": None, # Device name
46
+ "SERIAL_NUMBER": None, # Serial number
47
+ "DEVICE_ID": "01220000095B_DEVICE", # Device ID, i.e., SERIAL_NUMBER + "_DEVICE"
48
+ "MODEL": None, # Device model
49
+ "MODEL_ID": "PDPR1H1HAW100", # Model ID
50
+ "OWNERID": None, # Owner ID
51
+ "GROUPNAME": None, # Group name
52
+ "FW_VERSION": None, # Firmware version
53
+ "SW_VERSION": None, # Software version
54
+ "API_VERSION": None, # API version
55
+ "FW_CODE": "539187", # Firmware code
56
+ "MAC": None, # MAC address
57
+ "IP": None, # IP address
58
+ "WIFI_SSID": None, # WiFi SSID
59
+ "WIFI_KEY": None, # WiFi key
60
+ "AP_SSID": None, # Access Point SSID
61
+ "AP_KEY": None, # Access Point key
62
+ }
63
+
64
+ # Mapping-Status und Mapping-Cache
65
+ self._mapping_status = None
66
+ self._mapping_info: Optional[MappingInfo] = None
67
+
68
+ @classmethod
69
+ async def create(cls, host: str, timeout: int = 10, include_sensitive_data: bool = False) -> tuple[RequestStatus, "PooldoseClient" | None]:
70
+ """
71
+ Asynchronous factory method to initialize the Pooldose client.
72
+
73
+ Args:
74
+ host (str): The host address of the Pooldose device.
75
+ timeout (int): Timeout for API requests in seconds.
76
+ include_sensitive_data (bool): If True, fetch WiFi and AP keys.
77
+
78
+ Returns:
79
+ tuple: (RequestStatus, PooldoseClient|None) - Status and client instance.
80
+ """
81
+ self = cls(host, timeout)
82
+ status, handler = await RequestHandler.create(host, timeout)
83
+ if status != RequestStatus.SUCCESS:
84
+ _LOGGER.error("Failed to create RequestHandler: %s", status)
85
+ return status, None
86
+ self._request_handler = handler
87
+
88
+ # Fetch core parameters and device info
89
+ self.device_info["API_VERSION"] = self._request_handler.api_version
90
+
91
+ status, debug_config = await self._request_handler.get_debug_config()
92
+ if status != RequestStatus.SUCCESS or not debug_config:
93
+ _LOGGER.error("Failed to fetch debug config: %s", status)
94
+ return status, None
95
+ if (gateway := debug_config.get("GATEWAY")) is not None:
96
+ self.device_info["SERIAL_NUMBER"] = gateway.get("DID")
97
+ self.device_info["NAME"] = gateway.get("NAME")
98
+ self.device_info["SW_VERSION"] = gateway.get("FW_REL")
99
+ if (device := debug_config.get("DEVICES")[0]) is not None:
100
+ self.device_info["DEVICE_ID"] = device.get("DID")
101
+ self.device_info["MODEL"] = device.get("NAME")
102
+ self.device_info["MODEL_ID"] = device.get("PRODUCT_CODE")
103
+ self.device_info["FW_VERSION"] = device.get("FW_REL")
104
+ self.device_info["FW_CODE"] = device.get("FW_CODE")
105
+ await asyncio.sleep(0.5)
106
+
107
+ # Mapping laden, sobald MODEL_ID und FW_CODE verfügbar sind (asynchron!)
108
+ self._mapping_info = await MappingInfo.load(
109
+ self.device_info.get("MODEL_ID"),
110
+ self.device_info.get("FW_CODE")
111
+ )
112
+
113
+ # WiFi station info
114
+ status, wifi_station = await self._request_handler.get_wifi_station()
115
+ if status != RequestStatus.SUCCESS or not wifi_station:
116
+ _LOGGER.warning("Failed to fetch WiFi station info: %s", status)
117
+ else:
118
+ self.device_info["WIFI_SSID"] = wifi_station.get("SSID")
119
+ self.device_info["MAC"] = wifi_station.get("MAC")
120
+ self.device_info["IP"] = wifi_station.get("IP")
121
+ # Only include WiFi key if explicitly requested
122
+ if include_sensitive_data:
123
+ self.device_info["WIFI_KEY"] = wifi_station.get("KEY")
124
+ await asyncio.sleep(0.5)
125
+
126
+ # Access point info
127
+ status, access_point = await self._request_handler.get_access_point()
128
+ if status != RequestStatus.SUCCESS or not access_point:
129
+ _LOGGER.warning("Failed to fetch access point info: %s", status)
130
+ else:
131
+ self.device_info["AP_SSID"] = access_point.get("SSID")
132
+ # Only include AP key if explicitly requested
133
+ if include_sensitive_data:
134
+ self.device_info["AP_KEY"] = access_point.get("KEY")
135
+ await asyncio.sleep(0.5)
136
+
137
+ # Network info
138
+ status, network_info = await self._request_handler.get_network_info()
139
+ if status != RequestStatus.SUCCESS or not network_info:
140
+ _LOGGER.error("Failed to fetch network info: %s", status)
141
+ return status, None
142
+ self.device_info["OWNERID"] = network_info.get("OWNERID")
143
+ self.device_info["GROUPNAME"] = network_info.get("GROUPNAME")
144
+
145
+ if not include_sensitive_data:
146
+ _LOGGER.info("Excluded WiFi and AP keys (use include_sensitive_data=True to include)")
147
+
148
+ _LOGGER.debug("Initialized Pooldose client with device info: %s", self.device_info)
149
+ return RequestStatus.SUCCESS, self
150
+
151
+ def static_values(self) -> tuple[RequestStatus, StaticValues | None]:
152
+ """
153
+ Get the static device values as a StaticValues object.
154
+
155
+ Returns:
156
+ tuple: (RequestStatus, StaticValues|None) - Status and static values object.
157
+ """
158
+ try:
159
+ return RequestStatus.SUCCESS, StaticValues(self.device_info)
160
+ except (ValueError, TypeError, KeyError) as err:
161
+ _LOGGER.warning("Error creating StaticValues: %s", err)
162
+ return RequestStatus.UNKNOWN_ERROR, None
163
+
164
+ def available_types(self) -> dict[str, list[str]]:
165
+ """
166
+ Returns a dictionary mapping type categories to lists of available type names.
167
+
168
+ Returns:
169
+ dict[str, list[str]]: A dictionary where each key is a type category (as a string),
170
+ and each value is a list of available type names (as strings). If no mapping information
171
+ is available, returns an empty dictionary.
172
+ """
173
+ return self._mapping_info.available_types() if self._mapping_info else {}
174
+
175
+ def available_sensors(self) -> dict[str, SensorMapping]:
176
+ """
177
+ Returns all available sensors from the mapping as SensorMapping objects.
178
+ """
179
+ return self._mapping_info.available_sensors() if self._mapping_info else {}
180
+
181
+ def available_binary_sensors(self) -> dict[str, BinarySensorMapping]:
182
+ """
183
+ Returns all available binary sensors from the mapping as BinarySensorMapping objects.
184
+ """
185
+ return self._mapping_info.available_binary_sensors() if self._mapping_info else {}
186
+
187
+ def available_numbers(self) -> dict[str, NumberMapping]:
188
+ """
189
+ Returns all available numbers from the mapping as NumberMapping objects.
190
+ """
191
+ return self._mapping_info.available_numbers() if self._mapping_info else {}
192
+
193
+ def available_switches(self) -> dict[str, SwitchMapping]:
194
+ """
195
+ Returns all available switches from the mapping as SwitchMapping objects.
196
+ """
197
+ return self._mapping_info.available_switches() if self._mapping_info else {}
198
+
199
+ def available_selects(self) -> dict[str, SelectMapping]:
200
+ """
201
+ Returns all available selects from the mapping as SelectMapping objects.
202
+ """
203
+ return self._mapping_info.available_selects() if self._mapping_info else {}
204
+
205
+ async def instant_values(self) -> tuple[RequestStatus, InstantValues | None]:
206
+ """
207
+ Fetch the current instant values from the Pooldose device.
208
+
209
+ Returns:
210
+ tuple: (RequestStatus, InstantValues|None) - Status and instant values object.
211
+ """
212
+ try:
213
+ status, raw_data = await self._request_handler.get_values_raw()
214
+ if status != RequestStatus.SUCCESS or raw_data is None:
215
+ return status, None
216
+ # Mapping aus Cache verwenden
217
+ mapping = self._mapping_info.mapping if self._mapping_info else None
218
+ if mapping is None:
219
+ return RequestStatus.UNKNOWN_ERROR, None
220
+ device_id = self.device_info["DEVICE_ID"]
221
+ device_raw_data = raw_data.get("devicedata", {}).get(device_id, {})
222
+ model_id = self.device_info["MODEL_ID"]
223
+ fw_code = self.device_info["FW_CODE"]
224
+ prefix = f"{model_id}_FW{fw_code}_"
225
+ return RequestStatus.SUCCESS, InstantValues(device_raw_data, mapping, prefix, device_id, self._request_handler)
226
+ except (KeyError, TypeError, ValueError) as err:
227
+ _LOGGER.warning("Error creating InstantValues: %s", err)
228
+ return RequestStatus.UNKNOWN_ERROR, None
@@ -0,0 +1 @@
1
+ """Mappings for async API client for SEKO Pooldose."""
@@ -0,0 +1,222 @@
1
+ """Mapping Parser for async API client for SEKO Pooldose."""
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Any, Dict, Optional
5
+ import importlib.resources
6
+ import json
7
+ import logging
8
+ import aiofiles
9
+ from pooldose.request_handler import RequestStatus
10
+
11
+ # pylint: disable=line-too-long
12
+
13
+ _LOGGER = logging.getLogger(__name__)
14
+
15
+ @dataclass
16
+ class SensorMapping:
17
+ """
18
+ Represents a sensor mapping entry.
19
+ Attributes:
20
+ key (str): The key for the sensor.
21
+ type (str): The type, always "sensor".
22
+ conversion (Optional[dict]): Optional conversion mapping.
23
+ """
24
+ key: str
25
+ type: str
26
+ conversion: Optional[dict] = None
27
+
28
+ @dataclass
29
+ class BinarySensorMapping:
30
+ """
31
+ Represents a binary sensor mapping entry.
32
+ Attributes:
33
+ key (str): The key for the binary sensor.
34
+ type (str): The type, always "binary_sensor".
35
+ """
36
+ key: str
37
+ type: str
38
+
39
+ @dataclass
40
+ class NumberMapping:
41
+ """
42
+ Represents a number mapping entry.
43
+ Attributes:
44
+ key (str): The key for the number.
45
+ type (str): The type, always "number".
46
+ """
47
+ key: str
48
+ type: str
49
+
50
+ @dataclass
51
+ class SwitchMapping:
52
+ """
53
+ Represents a switch mapping entry.
54
+ Attributes:
55
+ key (str): The key for the switch.
56
+ type (str): The type, always "switch".
57
+ """
58
+ key: str
59
+ type: str
60
+
61
+ @dataclass
62
+ class SelectMapping:
63
+ """
64
+ Represents a select mapping entry.
65
+ Attributes:
66
+ key (str): The key for the select.
67
+ type (str): The type, always "select".
68
+ conversion (dict): Mandatory conversion mapping.
69
+ options (dict): Mandatory options mapping.
70
+ """
71
+ key: str
72
+ type: str
73
+ conversion: dict
74
+ options: dict
75
+
76
+ @dataclass
77
+ class MappingInfo:
78
+ """
79
+ Provides utilities to load and query mapping configurations for different models and firmware codes.
80
+
81
+ Attributes:
82
+ mapping (Optional[Dict[str, Any]]): The loaded mapping configuration, or None if not loaded.
83
+ status (Optional[RequestStatus]): The status of the mapping load operation.
84
+ """
85
+ mapping: Optional[Dict[str, Any]] = None
86
+ status: Optional[RequestStatus] = None
87
+
88
+ @classmethod
89
+ async def load(cls, model_id: str, fw_code: str) -> "MappingInfo":
90
+ """
91
+ Asynchronously load the model-specific mapping configuration from a JSON file.
92
+
93
+ Args:
94
+ model_id (str): The model ID.
95
+ fw_code (str): The firmware code.
96
+
97
+ Returns:
98
+ MappingInfo: The loaded mapping info object.
99
+ """
100
+ try:
101
+ if not model_id or not fw_code:
102
+ _LOGGER.error("MODEL_ID or FW_CODE not set!")
103
+ return cls(mapping=None, status=RequestStatus.NO_DATA)
104
+ filename = f"model_{model_id}_FW{fw_code}.json"
105
+ path = importlib.resources.files("pooldose.mappings").joinpath(filename)
106
+ async with aiofiles.open(path, "r", encoding="utf-8") as f:
107
+ content = await f.read()
108
+ mapping = json.loads(content)
109
+ return cls(mapping=mapping, status=RequestStatus.SUCCESS)
110
+ except (OSError, json.JSONDecodeError, ModuleNotFoundError, FileNotFoundError) as err:
111
+ _LOGGER.warning("Error loading model mapping: %s", err)
112
+ return cls(mapping=None, status=RequestStatus.UNKNOWN_ERROR)
113
+
114
+ def available_types(self) -> dict[str, list[str]]:
115
+ """
116
+ Returns all available types and their keys for the current model/firmware.
117
+
118
+ Returns:
119
+ dict[str, list[str]]: Mapping from type to list of keys.
120
+ """
121
+ if not self.mapping:
122
+ return {}
123
+ result = {}
124
+ for key, entry in self.mapping.items():
125
+ typ = entry.get("type", "unknown")
126
+ result.setdefault(typ, []).append(key)
127
+ return result
128
+
129
+ def available_sensors(self) -> Dict[str, SensorMapping]:
130
+ """
131
+ Returns all available sensors from the mapping as SensorMapping objects.
132
+
133
+ Returns:
134
+ Dict[str, SensorMapping]: Mapping from name to SensorMapping.
135
+ """
136
+ if not self.mapping:
137
+ return {}
138
+ result = {}
139
+ for name, entry in self.mapping.items():
140
+ if entry.get("type") == "sensor":
141
+ result[name] = SensorMapping(
142
+ key=entry["key"],
143
+ type=entry["type"],
144
+ conversion=entry.get("conversion"),
145
+ )
146
+ return result
147
+
148
+ def available_binary_sensors(self) -> Dict[str, BinarySensorMapping]:
149
+ """
150
+ Returns all available binary sensors from the mapping as BinarySensorMapping objects.
151
+
152
+ Returns:
153
+ Dict[str, BinarySensorMapping]: Mapping from name to BinarySensorMapping.
154
+ """
155
+ if not self.mapping:
156
+ return {}
157
+ result = {}
158
+ for name, entry in self.mapping.items():
159
+ if entry.get("type") == "binary_sensor":
160
+ result[name] = BinarySensorMapping(
161
+ key=entry["key"],
162
+ type=entry["type"],
163
+ )
164
+ return result
165
+
166
+ def available_numbers(self) -> Dict[str, NumberMapping]:
167
+ """
168
+ Returns all available numbers from the mapping as NumberMapping objects.
169
+
170
+ Returns:
171
+ Dict[str, NumberMapping]: Mapping from name to NumberMapping.
172
+ """
173
+ if not self.mapping:
174
+ return {}
175
+ result = {}
176
+ for name, entry in self.mapping.items():
177
+ if entry.get("type") == "number":
178
+ result[name] = NumberMapping(
179
+ key=entry["key"],
180
+ type=entry["type"],
181
+ )
182
+ return result
183
+
184
+ def available_switches(self) -> Dict[str, SwitchMapping]:
185
+ """
186
+ Returns all available switches from the mapping as SwitchMapping objects.
187
+
188
+ Returns:
189
+ Dict[str, SwitchMapping]: Mapping from name to SwitchMapping.
190
+ """
191
+ if not self.mapping:
192
+ return {}
193
+ result = {}
194
+ for name, entry in self.mapping.items():
195
+ if entry.get("type") == "switch":
196
+ result[name] = SwitchMapping(
197
+ key=entry["key"],
198
+ type=entry["type"],
199
+ )
200
+ return result
201
+
202
+ def available_selects(self) -> Dict[str, SelectMapping]:
203
+ """
204
+ Returns all available selects from the mapping as SelectMapping objects.
205
+
206
+ Returns:
207
+ Dict[str, SelectMapping]: Mapping from name to SelectMapping.
208
+ Raises:
209
+ KeyError: If a select entry does not contain 'conversion' or 'options'.
210
+ """
211
+ if not self.mapping:
212
+ return {}
213
+ result = {}
214
+ for name, entry in self.mapping.items():
215
+ if entry.get("type") == "select":
216
+ result[name] = SelectMapping(
217
+ key=entry["key"],
218
+ type=entry["type"],
219
+ conversion=entry["conversion"],
220
+ options=entry["options"],
221
+ )
222
+ return result
@@ -0,0 +1,153 @@
1
+ {
2
+ "temperature": {
3
+ "key": "w_1eommf39k",
4
+ "type": "sensor"
5
+ },
6
+ "ph": {
7
+ "key": "w_1ekeigkin",
8
+ "type": "sensor"
9
+ },
10
+ "orp": {
11
+ "key": "w_1eklenb23",
12
+ "type": "sensor"
13
+ },
14
+ "ph_type_dosing": {
15
+ "key": "w_1eklg44ro",
16
+ "type": "sensor",
17
+ "conversion": {
18
+ "|PDPR1H1HAW100_FW539187_LABEL_w_1eklg44ro_ALCALYNE|": "alcalyne",
19
+ "|PDPR1H1HAW100_FW539187_LABEL_w_1eklg44ro_ACID|" : "acid"
20
+ }
21
+ },
22
+ "peristaltic_ph_dosing": {
23
+ "key": "w_1eklj6euj",
24
+ "type": "sensor",
25
+ "conversion": {
26
+ "|PDPR1H1HAW100_FW539187_LABEL_w_1eklj6euj_OFF|": "off",
27
+ "|PDPR1H1HAW100_FW539187_LABEL_w_1eklj6euj_PROPORTIONAL|": "proportional",
28
+ "|PDPR1H1HAW100_FW539187_LABEL_w_1eklj6euj_ON_OFF|": "on_off",
29
+ "|PDPR1H1HAW100_FW539187_LABEL_w_1eklj6euj_TIMED|": "timed"
30
+ }
31
+ },
32
+ "ofa_ph_value": {
33
+ "key": "w_1eo1ttmft",
34
+ "type": "sensor"
35
+ },
36
+ "orp_type_dosing": {
37
+ "key": "w_1eklgnolb",
38
+ "type": "sensor",
39
+ "conversion": {
40
+ "|PDPR1H1HAW100_FW539187_LABEL_w_1eklgnolb_LOW|": "low",
41
+ "|PDPR1H1HAW100_FW539187_LABEL_w_1eklgnolb_HIGH|": "high"
42
+ }
43
+ },
44
+ "peristaltic_orp_dosing": {
45
+ "key": "w_1eo1s18s8",
46
+ "type": "sensor",
47
+ "conversion": {
48
+ "|PDPR1H1HAW100_FW539187_LABEL_w_1eo1s18s8_OFF|": "off",
49
+ "|PDPR1H1HAW100_FW539187_LABEL_w_1eo1s18s8_PROPORTIONAL|": "proportional",
50
+ "|PDPR1H1HAW100_FW539187_LABEL_w_1eo1s18s8_ON_OFF|": "on_off",
51
+ "|PDPR1H1HAW100_FW539187_LABEL_w_1eo1s18s8_TIMED|": "timed"
52
+ }
53
+ },
54
+ "ofa_orp_value": {
55
+ "key": "w_1eo1tui1d",
56
+ "type": "sensor"
57
+ },
58
+ "ph_calibration_type": {
59
+ "key": "w_1eklh8gb7",
60
+ "type": "sensor",
61
+ "conversion": {
62
+ "|PDPR1H1HAW100_FW539187_LABEL_w_1eklh8gb7_OFF|": "off",
63
+ "|PDPR1H1HAW100_FW539187_LABEL_w_1eklh8gb7_REFERENCE|": "reference",
64
+ "|PDPR1H1HAW100_FW539187_LABEL_w_1eklh8gb7_1_POINT|": "1_point",
65
+ "|PDPR1H1HAW100_FW539187_LABEL_w_1eklh8gb7_2_POINTS|": "2_points"
66
+ }
67
+ },
68
+ "ph_calibration_offset": {
69
+ "key": "w_1eklhs3b4",
70
+ "type": "sensor"
71
+ },
72
+ "ph_calibration_slope": {
73
+ "key": "w_1eklhs65u",
74
+ "type": "sensor"
75
+ },
76
+ "orp_calibration_type": {
77
+ "key": "w_1eklh8i5t",
78
+ "type": "sensor",
79
+ "conversion": {
80
+ "|PDPR1H1HAW100_FW539187_LABEL_w_1eklh8i5t_OFF|": "off",
81
+ "|PDPR1H1HAW100_FW539187_LABEL_w_1eklh8i5t_REFERENCE|": "reference",
82
+ "|PDPR1H1HAW100_FW539187_LABEL_w_1eklh8i5t_1_POINT|": "1_point"
83
+ }
84
+ },
85
+ "orp_calibration_offset": {
86
+ "key": "w_1eklhs8r3",
87
+ "type": "sensor"
88
+ },
89
+ "orp_calibration_slope": {
90
+ "key": "w_1eklhsase",
91
+ "type": "sensor"
92
+ },
93
+ "pump_running": {
94
+ "key": "w_1ekga097n",
95
+ "type": "binary_sensor"
96
+ },
97
+ "ph_level_ok": {
98
+ "key": "w_1eklf77pm",
99
+ "type": "binary_sensor"
100
+ },
101
+ "orp_level_ok": {
102
+ "key": "w_1eo04bcr2",
103
+ "type": "binary_sensor"
104
+ },
105
+ "flow_rate_ok": {
106
+ "key": "w_1eo04nc5n",
107
+ "type": "binary_sensor"
108
+ },
109
+ "alarm_relay": {
110
+ "key": "w_1eklffdl0",
111
+ "type": "binary_sensor"
112
+ },
113
+ "relay_aux1_ph": {
114
+ "key": "w_1eoi2rv4h",
115
+ "type": "binary_sensor"
116
+ },
117
+ "relay_aux2_orpcl": {
118
+ "key": "w_1eoi2s16b",
119
+ "type": "binary_sensor"
120
+ },
121
+ "ph_target": {
122
+ "key": "w_1ekeiqfat",
123
+ "type": "number"
124
+ },
125
+ "orp_target": {
126
+ "key": "w_1eklgnjk2",
127
+ "type": "number"
128
+ },
129
+ "stop_pool_dosing": {
130
+ "key": "w_1emtltkel",
131
+ "type": "switch"
132
+ },
133
+ "pump_detection": {
134
+ "key": "w_1eklft47q",
135
+ "type": "switch"
136
+ },
137
+ "frequency_input": {
138
+ "key": "w_1eklft5qt",
139
+ "type": "switch"
140
+ },
141
+ "water_meter_unit": {
142
+ "key": "w_1eklinki6",
143
+ "type": "select",
144
+ "options": {
145
+ "0": "PDPR1H1HAW100_FW539187_COMBO_w_1eklinki6_M_",
146
+ "1": "PDPR1H1HAW100_FW539187_COMBO_w_1eklinki6_LITER"
147
+ },
148
+ "conversion": {
149
+ "PDPR1H1HAW100_FW539187_COMBO_w_1eklinki6_M_": "m³",
150
+ "PDPR1H1HAW100_FW539187_COMBO_w_1eklinki6_LITER": "L"
151
+ }
152
+ }
153
+ }