• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

nielstron / pyfronius / 17707784515

14 Sep 2025 06:58AM UTC coverage: 80.029%. First build
17707784515

Pull #27

github

web-flow
Merge 31aefb8b0 into 08129a808
Pull Request #27: Fix broken checks from PR #25

45 of 58 new or added lines in 1 file covered. (77.59%)

549 of 686 relevant lines covered (80.03%)

4.8 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

79.94
/pyfronius/__init__.py
1
"""
2
Created on 27.09.2017
3

4
@author: Niels
5
@author: Gerrit Beine
6
"""
7

8
import asyncio
6✔
9
import enum
6✔
10
import json
6✔
11
import logging
6✔
12
from html import unescape
6✔
13
from typing import Any, Callable, Dict, Final, Iterable, List, Tuple, Union
6✔
14

15
import aiohttp
6✔
16

17
from .const import INVERTER_DEVICE_TYPE, OHMPILOT_STATE_CODES
6✔
18

19
_LOGGER = logging.getLogger(__name__)
6✔
20
DEGREE_CELSIUS: Final = "°C"
6✔
21
WATT: Final = "W"
6✔
22
WATT_HOUR: Final = "Wh"
6✔
23
AMPERE: Final = "A"
6✔
24
VOLT: Final = "V"
6✔
25
PERCENT: Final = "%"
6✔
26
HERTZ: Final = "Hz"
6✔
27
VOLTAMPEREREACTIVE: Final = "VAr"
6✔
28
VOLTAMPEREREACTIVE_HOUR: Final = "VArh"
6✔
29
VOLTAMPERE: Final = "VA"
6✔
30

31

32
class API_VERSION(enum.Enum):
6✔
33
    value: int
6✔
34

35
    AUTO = -1
6✔
36
    V0 = 0
6✔
37
    V1 = 1
6✔
38

39

40
API_BASEPATHS: Final = {
6✔
41
    API_VERSION.V0: "/solar_api/",
42
    API_VERSION.V1: "/solar_api/v1/",
43
}
44

45
URL_API_VERSION: Final = "solar_api/GetAPIVersion.cgi"
6✔
46
URL_POWER_FLOW: Final = {API_VERSION.V1: "GetPowerFlowRealtimeData.fcgi"}
6✔
47
URL_SYSTEM_METER: Final = {API_VERSION.V1: "GetMeterRealtimeData.cgi?Scope=System"}
6✔
48
URL_SYSTEM_INVERTER: Final = {
6✔
49
    API_VERSION.V0: "GetInverterRealtimeData.cgi?Scope=System",
50
    API_VERSION.V1: "GetInverterRealtimeData.cgi?Scope=System",
51
}
52
URL_SYSTEM_LED: Final = {API_VERSION.V1: "GetLoggerLEDInfo.cgi"}
6✔
53
URL_SYSTEM_OHMPILOT: Final = {
6✔
54
    API_VERSION.V1: "GetOhmPilotRealtimeData.cgi?Scope=System"
55
}
56
URL_SYSTEM_STORAGE: Final = {API_VERSION.V1: "GetStorageRealtimeData.cgi?Scope=System"}
6✔
57
URL_DEVICE_METER: Final = {
6✔
58
    API_VERSION.V1: "GetMeterRealtimeData.cgi?Scope=Device&DeviceId={}"
59
}
60
URL_DEVICE_STORAGE: Final = {
6✔
61
    API_VERSION.V1: "GetStorageRealtimeData.cgi?Scope=Device&DeviceId={}"
62
}
63
URL_DEVICE_INVERTER_CUMULATIVE: Final = {
6✔
64
    API_VERSION.V0: (
65
        "GetInverterRealtimeData.cgi?Scope=Device&"
66
        "DeviceIndex={}&"
67
        "DataCollection=CumulationInverterData"
68
    ),
69
    API_VERSION.V1: (
70
        "GetInverterRealtimeData.cgi?Scope=Device&"
71
        "DeviceId={}&"
72
        "DataCollection=CumulationInverterData"
73
    ),
74
}
75
URL_DEVICE_INVERTER_COMMON: Final = {
6✔
76
    API_VERSION.V0: (
77
        "GetInverterRealtimeData.cgi?Scope=Device&"
78
        "DeviceIndex={}&"
79
        "DataCollection=CommonInverterData"
80
    ),
81
    API_VERSION.V1: (
82
        "GetInverterRealtimeData.cgi?Scope=Device&"
83
        "DeviceId={}&"
84
        "DataCollection=CommonInverterData"
85
    ),
86
}
87
URL_DEVICE_INVERTER_3P: Final = {
6✔
88
    API_VERSION.V1: (
89
        "GetInverterRealtimeData.cgi?Scope=Device&"
90
        "DeviceId={}&"
91
        "DataCollection=3PInverterData"
92
    ),
93
}
94
URL_ACTIVE_DEVICE_INFO_SYSTEM: Final = {
6✔
95
    API_VERSION.V1: "GetActiveDeviceInfo.cgi?DeviceClass=System"
96
}
97
URL_INVERTER_INFO: Final = {
6✔
98
    API_VERSION.V0: "GetInverterInfo.cgi",
99
    API_VERSION.V1: "GetInverterInfo.cgi",
100
}
101
URL_LOGGER_INFO: Final = {
6✔
102
    API_VERSION.V0: "GetLoggerInfo.cgi",
103
    API_VERSION.V1: "GetLoggerInfo.cgi",
104
}
105

106
HEADER_STATUS_CODES: Final = {
6✔
107
    0: "OKAY",
108
    1: "NotImplemented",
109
    2: "Uninitialized",
110
    3: "Initialized",
111
    4: "Running",
112
    5: "Timeout",
113
    6: "Argument Error",
114
    7: "LNRequestError",
115
    8: "LNRequestTimeout",
116
    9: "LNParseError",
117
    10: "ConfigIOError",
118
    11: "NotSupported",
119
    12: "DeviceNotAvailable",
120
    255: "UnknownError",
121
}
122

123

124
class FroniusError(Exception):
6✔
125
    """
126
    A superclass that covers all errors occuring during the
127
    connection to a Fronius device
128
    """
129

130

131
class NotSupportedError(ValueError, FroniusError):
6✔
132
    """
133
    An error to be raised if a specific feature
134
    is not supported by the specified device
135
    """
136

137

138
class FroniusConnectionError(ConnectionError, FroniusError):
6✔
139
    """
140
    An error to be raised if the connection to the fronius device failed
141
    """
142

143

144
class InvalidAnswerError(ValueError, FroniusError):
6✔
145
    """
146
    An error to be raised if the host Fronius device could not answer a request
147
    """
148

149

150
class BadStatusError(FroniusError):
6✔
151
    """A bad status code was returned."""
152

153
    def __init__(
6✔
154
        self,
155
        endpoint: str,
156
        code: int,
157
        reason: Union[str, None] = None,
158
        response: Dict[str, Any] = {},
159
    ) -> None:
160
        """Instantiate exception."""
161
        self.response = response
×
162
        message = (
×
163
            f"BadStatusError at {endpoint}. "
164
            f"Code: {code} - {HEADER_STATUS_CODES.get(code, 'unknown status code')}. "
165
            f"Reason: {reason or 'unknown'}."
166
        )
167
        super().__init__(message)
×
168

169

170
class Fronius:
6✔
171
    """
172
    Interface to communicate with the Fronius Symo over http / JSON
173
    Timeouts are to be set in the given AIO session
174
    Attributes:
175
        session     The AIO session
176
        url         The url for reaching of the Fronius device
177
                    (i.e. http://192.168.0.10:80)
178
        api_version  Version of Fronius API to use
179
    """
180

181
    def __init__(
6✔
182
        self,
183
        session: aiohttp.ClientSession,
184
        url: str,
185
        api_version: API_VERSION = API_VERSION.AUTO,
186
    ) -> None:
187
        """
188
        Constructor
189
        """
190
        self._aio_session = session
6✔
191
        while url[-1] == "/":
6✔
192
            url = url[:-1]
×
193
        self.url = url
6✔
194
        # prepend http:// if missing, by fronius API this is the only supported protocol
195
        if not self.url.startswith("http"):
6✔
196
            self.url = "http://{}".format(self.url)
×
197
        self.api_version = api_version
6✔
198
        self.base_url = API_BASEPATHS.get(api_version)
6✔
199

200
    async def _fetch_json(self, url: str) -> Dict[str, Any]:
6✔
201
        """
202
        Fetch json value from fixed url
203
        """
204
        result: Dict[str, Any]
205
        try:
6✔
206
            async with self._aio_session.get(url) as res:
6✔
207
                result = await res.json(content_type=None)
6✔
208
        except asyncio.TimeoutError:
6✔
209
            raise FroniusConnectionError(
×
210
                "Connection to Fronius device timed out at {}.".format(url)
211
            )
212
        except aiohttp.ClientError:
6✔
213
            raise FroniusConnectionError(
6✔
214
                "Connection to Fronius device failed at {}.".format(url)
215
            )
216
        except (aiohttp.ContentTypeError, json.decoder.JSONDecodeError):
6✔
217
            raise InvalidAnswerError(
6✔
218
                "Host returned a non-JSON reply at {}.".format(url)
219
            )
220
        return result
6✔
221

222
    async def fetch_api_version(self) -> Tuple[API_VERSION, str]:
6✔
223
        """
224
        Fetches the highest supported API version of the initiated fronius device
225
        :return:
226
        """
227
        try:
6✔
228
            res = await self._fetch_json("{}/{}".format(self.url, URL_API_VERSION))
6✔
229
            api_version, base_url = API_VERSION(res["APIVersion"]), res["BaseURL"]
6✔
230
        except InvalidAnswerError:
6✔
231
            # Host returns 404 response if API version is 0
232
            api_version, base_url = API_VERSION.V0, API_BASEPATHS[API_VERSION.V0]
6✔
233

234
        return api_version, base_url
6✔
235

236
    async def _fetch_solar_api(
6✔
237
        self, spec: Dict[API_VERSION, str], spec_name: str, *spec_formattings: str
238
    ) -> Dict[str, Any]:
239
        """
240
        Fetch page of solar_api
241
        """
242
        # either unknown api version given or automatic
243
        if self.base_url is None:
6✔
244
            prev_api_version = self.api_version
6✔
245
            self.api_version, self.base_url = await self.fetch_api_version()
6✔
246
            if prev_api_version == API_VERSION.AUTO:
6✔
247
                _LOGGER.debug(
6✔
248
                    """using highest supported API version {}""".format(
249
                        self.api_version
250
                    )
251
                )
252
            if (
6✔
253
                prev_api_version != self.api_version
254
                and prev_api_version != API_VERSION.AUTO
255
            ):
256
                _LOGGER.warning(
×
257
                    (
258
                        """Unknown API version {} is not supported by host {},"""
259
                        """using highest supported API version {} instead"""
260
                    ).format(prev_api_version, self.url, self.api_version)
261
                )
262
        spec_url = spec.get(self.api_version)
6✔
263
        if spec_url is None:
6✔
264
            raise NotSupportedError(
6✔
265
                "API version {} does not support request of {} data".format(
266
                    self.api_version, spec_name
267
                )
268
            )
269
        if spec_formattings:
6✔
270
            spec_url = spec_url.format(*spec_formattings)
6✔
271

272
        _LOGGER.debug("Get {} data for {}".format(spec_name, spec_url))
6✔
273
        res = await self._fetch_json("{}{}{}".format(self.url, self.base_url, spec_url))
6✔
274
        return res
6✔
275

276
    async def fetch(
6✔
277
        self,
278
        active_device_info: bool = True,
279
        inverter_info: bool = True,
280
        logger_info: bool = True,
281
        power_flow: bool = True,
282
        system_meter: bool = True,
283
        system_inverter: bool = True,
284
        system_ohmpilot: bool = True,
285
        system_storage: bool = True,
286
        device_meter: Iterable[str] = frozenset(["0"]),
287
        # storage is not necessarily supported by every fronius device
288
        device_storage: Iterable[str] = frozenset(["0"]),
289
        device_inverter: Iterable[str] = frozenset(["1"]),
290
    ) -> List[Dict[str, Any]]:
291
        requests = []
6✔
292
        if active_device_info:
6✔
293
            requests.append(self.current_active_device_info())
6✔
294
        if inverter_info:
6✔
295
            requests.append(self.inverter_info())
6✔
296
        if logger_info:
6✔
297
            requests.append(self.current_logger_info())
6✔
298
        if power_flow:
6✔
299
            requests.append(self.current_power_flow())
6✔
300
        if system_meter:
6✔
301
            requests.append(self.current_system_meter_data())
6✔
302
        if system_inverter:
6✔
303
            requests.append(self.current_system_inverter_data())
6✔
304
        if system_ohmpilot:
6✔
305
            requests.append(self.current_system_ohmpilot_data())
6✔
306
        if system_storage:
6✔
307
            requests.append(self.current_system_storage_data())
×
308
        for i in device_meter:
6✔
309
            requests.append(self.current_meter_data(i))
6✔
310
        for i in device_storage:
6✔
311
            requests.append(self.current_storage_data(i))
6✔
312
        for i in device_inverter:
6✔
313
            requests.append(self.current_inverter_data(i))
6✔
314
        for i in device_inverter:
6✔
315
            requests.append(self.current_inverter_3p_data(i))
6✔
316

317
        res = await asyncio.gather(*requests, return_exceptions=True)
6✔
318
        responses = []
6✔
319
        for result in res:
6✔
320
            if isinstance(result, (FroniusError, BaseException)):
6✔
321
                _LOGGER.warning(result)
×
322
                if isinstance(result, BadStatusError):
×
323
                    responses.append(result.response)
×
324
                continue
×
325
            responses.append(result)
6✔
326
        return responses
6✔
327

328
    @staticmethod
6✔
329
    def _status_data(res: Dict[str, Any]) -> Dict[str, Any]:
6✔
330
        sensor = {}
6✔
331

332
        sensor["timestamp"] = {"value": res["Head"]["Timestamp"]}
6✔
333
        sensor["status"] = res["Head"]["Status"]
6✔
334

335
        return sensor
6✔
336

337
    @staticmethod
6✔
338
    def error_code(sensor_data: Dict[str, Any]) -> Any:
6✔
339
        """
340
        Extract error code from returned sensor data
341
        :param sensor_data: Dictionary returned as current data
342
        """
343
        return sensor_data["status"]["Code"]
6✔
344

345
    @staticmethod
6✔
346
    def error_reason(sensor_data: Dict[str, Any]) -> Any:
6✔
347
        """
348
        Extract error reason from returned sensor data
349
        :param sensor_data: Dictionary returned as current data
350
        """
351
        return sensor_data["status"]["Reason"]
6✔
352

353
    async def _current_data(
6✔
354
        self,
355
        fun: Callable[[Dict[str, Any]], Dict[str, Any]],
356
        spec: Dict[API_VERSION, str],
357
        spec_name: str,
358
        *spec_formattings: str,
359
    ) -> Dict[str, Any]:
360
        sensor = {}
6✔
361
        try:
6✔
362
            res = await self._fetch_solar_api(spec, spec_name, *spec_formattings)
6✔
363
        except InvalidAnswerError:
6✔
364
            # except if Host returns 404
365
            raise NotSupportedError(
6✔
366
                "Device type {} not supported by the fronius device".format(spec_name)
367
            )
368

369
        try:
6✔
370
            sensor.update(Fronius._status_data(res))
6✔
371
        except (TypeError, KeyError):
×
372
            raise InvalidAnswerError(
×
373
                "No header data returned from {} ({})".format(spec, spec_formattings)
374
            )
375
        else:
376
            if sensor["status"]["Code"] != 0:
6✔
377
                endpoint = spec[self.api_version]
×
378
                code = sensor["status"]["Code"]
×
379
                reason = sensor["status"]["Reason"]
×
380
                raise BadStatusError(endpoint, code, reason=reason, response=sensor)
×
381
        try:
6✔
382
            sensor.update(fun(res["Body"]["Data"]))
6✔
383
        except (TypeError, KeyError):
6✔
384
            # LoggerInfo oddly deviates from the default scheme
385
            try:
6✔
386
                sensor.update(fun(res["Body"]["LoggerInfo"]))
6✔
387
            except (TypeError, KeyError):
×
388
                raise InvalidAnswerError(
×
389
                    "No body data returned from {} ({})".format(spec, spec_formattings)
390
                )
391
        return sensor
6✔
392

393
    async def current_power_flow(
6✔
394
            self,
395
            ext_cb_conversion: Callable[[Dict[str, Any]], Dict[str, Any]] = None
396
    ) -> Dict[str, Any]:
397
        """
398
        Get the current power flow of a smart meter system.
399
        """
400
        cb = Fronius._system_power_flow
6✔
401
        if ext_cb_conversion is not None:
6✔
NEW
402
            cb = ext_cb_conversion
×
403
        return await self._current_data(cb, URL_POWER_FLOW, "current power flow")
6✔
404

405
    async def current_system_meter_data(
6✔
406
            self,
407
            ext_cb_conversion: Callable[[Dict[str, Any]], Dict[str, Any]] = None
408
    ) -> Dict[str, Any]:
409
        """
410
        Get the current meter data.
411
        """
412
        cb = Fronius._system_meter_data
6✔
413
        if ext_cb_conversion is not None:
6✔
NEW
414
            cb = ext_cb_conversion
×
415
        return await self._current_data(cb, URL_SYSTEM_METER, "current system meter")
6✔
416

417
    async def current_system_inverter_data(
6✔
418
            self,
419
            ext_cb_conversion: Callable[[Dict[str, Any]], Dict[str, Any]] = None
420
    ) -> Dict[str, Any]:
421
        """
422
        Get the current inverter data.
423
        The values are provided as cumulated values and for each inverter
424
        """
425
        cb = Fronius._system_inverter_data
6✔
426
        if ext_cb_conversion is not None:
6✔
NEW
427
            cb = ext_cb_conversion
×
428
        return await self._current_data(
6✔
429
            cb, URL_SYSTEM_INVERTER, "current system inverter")
430

431
    async def current_system_ohmpilot_data(
6✔
432
            self,
433
            ext_cb_conversion: Callable[[Dict[str, Any]], Dict[str, Any]] = None
434
    ) -> Dict[str, Any]:
435
        """
436
        Get the current ohmpilot data.
437
        """
438
        cb = Fronius._system_ohmpilot_data
6✔
439
        if ext_cb_conversion is not None:
6✔
NEW
440
            cb = ext_cb_conversion
×
441
        return await self._current_data(
6✔
442
            cb, URL_SYSTEM_OHMPILOT, "current system ohmpilot")
443

444
    async def current_meter_data(
6✔
445
            self,
446
            device: str = "0",
447
            ext_cb_conversion: Callable[[Dict[str, Any]], Dict[str, Any]] = None
448
    ) -> Dict[str, Any]:
449
        """
450
        Get the current meter data for a device.
451
        """
452
        cb = Fronius._device_meter_data
6✔
453
        if ext_cb_conversion is not None:
6✔
NEW
454
            cb = ext_cb_conversion
×
455
        return await self._current_data(cb, URL_DEVICE_METER, "current meter", device)
6✔
456

457
    async def current_storage_data(
6✔
458
            self,
459
            device: str = "0",
460
            ext_cb_conversion: Callable[[Dict[str, Any]], Dict[str, Any]] = None
461
    ) -> Dict[str, Any]:
462
        """
463
        Get the current storage data for a device.
464
        Provides data about batteries.
465
        """
466
        cb = Fronius._device_storage_data
6✔
467
        if ext_cb_conversion is not None:
6✔
NEW
468
            cb = ext_cb_conversion
×
469
        return await self._current_data(
6✔
470
            cb, URL_DEVICE_STORAGE, "current storage", device)
471

472
    async def current_system_storage_data(
6✔
473
            self,
474
            ext_cb_conversion: Callable[[Dict[str, Any]], Dict[str, Any]] = None
475
    ) -> Dict[str, Any]:
476
        """
477
        Get the current storage data for a device.
478
        Provides data about batteries.
479
        """
480
        cb = Fronius._system_storage_data
6✔
481
        if ext_cb_conversion is not None:
6✔
NEW
482
            cb = ext_cb_conversion
×
483
        return await self._current_data(
6✔
484
            cb, URL_SYSTEM_STORAGE, "current system storage")
485

486
    async def current_inverter_data(
6✔
487
            self,
488
            device: str = "1",
489
            ext_cb_conversion: Callable[[Dict[str, Any]], Dict[str, Any]] = None
490
    ) -> Dict[str, Any]:
491
        """
492
        Get the current inverter data of one device.
493
        """
494
        cb = Fronius._device_inverter_data
6✔
495
        if ext_cb_conversion is not None:
6✔
NEW
496
            cb = ext_cb_conversion
×
497
        return await self._current_data(
6✔
498
            cb, URL_DEVICE_INVERTER_COMMON, "current inverter", device)
499

500
    async def current_inverter_3p_data(
6✔
501
            self,
502
            device: str = "1",
503
            ext_cb_conversion: Callable[[Dict[str, Any]], Dict[str, Any]] = None
504
    ) -> Dict[str, Any]:
505
        """
506
        Get the current inverter 3 phase data of one device.
507
        """
508
        cb = Fronius._device_inverter_3p_data
6✔
509
        if ext_cb_conversion is not None:
6✔
NEW
510
            cb = ext_cb_conversion
×
511
        return await self._current_data(
6✔
512
            cb, URL_DEVICE_INVERTER_3P, "current inverter 3p", device)
513

514
    async def current_led_data(
6✔
515
            self,
516
            ext_cb_conversion: Callable[[Dict[str, Any]], Dict[str, Any]] = None
517
    ) -> Dict[str, Any]:
518
        """
519
        Get the current info led data for all LEDs
520
        """
521
        cb = Fronius._system_led_data
6✔
522
        if ext_cb_conversion is not None:
6✔
NEW
523
            cb = ext_cb_conversion
×
524
        return await self._current_data(cb, URL_SYSTEM_LED, "current led")
6✔
525

526
    async def current_active_device_info(
6✔
527
            self,
528
            ext_cb_conversion: Callable[[Dict[str, Any]], Dict[str, Any]] = None
529
    ) -> Dict[str, Any]:
530
        """
531
        Get info about the current active devices in a smart meter system.
532
        """
533
        cb = Fronius._system_active_device_info
6✔
534
        if ext_cb_conversion is not None:
6✔
NEW
535
            cb = ext_cb_conversion
×
536
        return await self._current_data(
6✔
537
            cb, URL_ACTIVE_DEVICE_INFO_SYSTEM, "current active device info")
538

539
    async def current_logger_info(
6✔
540
            self,
541
            ext_cb_conversion: Callable[[Dict[str, Any]], Dict[str, Any]] = None
542
    ) -> Dict[str, Any]:
543
        """
544
        Get the current logger info of a smart meter system.
545
        """
546
        cb = Fronius._logger_info
6✔
547
        if ext_cb_conversion is not None:
6✔
NEW
548
            cb = ext_cb_conversion
×
549
        return await self._current_data(cb, URL_LOGGER_INFO, "current logger info")
6✔
550

551
    async def inverter_info(
6✔
552
            self,
553
            ext_cb_conversion: Callable[[Dict[str, Any]], Dict[str, Any]] = None
554
    ) -> Dict[str, Any]:
555
        """
556
        Get the general infos of an inverter.
557
        """
558
        cb = Fronius._inverter_info
6✔
559
        if ext_cb_conversion is not None:
6✔
NEW
560
            cb = ext_cb_conversion
×
561
        return await self._current_data(cb, URL_INVERTER_INFO, "inverter info")
6✔
562

563
    @staticmethod
6✔
564
    def _system_led_data(data: Dict[str, Any]) -> Dict[str, Any]:
6✔
565
        _LOGGER.debug("Converting system led data: '{}'".format(data))
6✔
566
        sensor = {}
6✔
567

568
        _map = {
6✔
569
            "PowerLED": "power_led",
570
            "SolarNetLED": "solar_net_led",
571
            "SolarWebLED": "solar_web_led",
572
            "WLANLED": "wlan_led",
573
        }
574

575
        for led in _map:
6✔
576
            if led in data:
6✔
577
                sensor[_map[led]] = {
6✔
578
                    "color": data[led]["Color"],
579
                    "state": data[led]["State"],
580
                }
581

582
        return sensor
6✔
583

584
    @staticmethod
6✔
585
    def _system_power_flow(data: Dict[str, Any]) -> Dict[str, Any]:
6✔
586
        _LOGGER.debug("Converting system power flow data: '{}'".format(data))
6✔
587
        sensor = {}
6✔
588

589
        site = data["Site"]
6✔
590
        # Backwards compatability
591
        if data["Inverters"].get("1"):
6✔
592
            inverter = data["Inverters"]["1"]
6✔
593
            if "Battery_Mode" in inverter:
6✔
594
                sensor["battery_mode"] = {"value": inverter["Battery_Mode"]}
×
595
            if "SOC" in inverter:
6✔
596
                sensor["state_of_charge"] = {"value": inverter["SOC"], "unit": PERCENT}
×
597

598
        for index, inverter in enumerate(data["Inverters"]):
6✔
599
            if "Battery_Mode" in inverter:
6✔
600
                sensor["battery_mode_{}".format(index)] = {
×
601
                    "value": inverter["Battery_Mode"]
602
                }
603
            if "SOC" in inverter:
6✔
604
                sensor["state_of_charge_{}".format(index)] = {
×
605
                    "value": inverter["SOC"],
606
                    "unit": PERCENT,
607
                }
608

609
        if "BackupMode" in site:
6✔
610
            sensor["backup_mode"] = {"value": site["BackupMode"]}
×
611
        if "BatteryStandby" in site:
6✔
612
            sensor["battery_standby"] = {"value": site["BatteryStandby"]}
×
613
        if "E_Day" in site:
6✔
614
            sensor["energy_day"] = {"value": site["E_Day"], "unit": WATT_HOUR}
6✔
615
        if "E_Total" in site:
6✔
616
            sensor["energy_total"] = {"value": site["E_Total"], "unit": WATT_HOUR}
6✔
617
        if "E_Year" in site:
6✔
618
            sensor["energy_year"] = {"value": site["E_Year"], "unit": WATT_HOUR}
6✔
619
        if "Meter_Location" in site:
6✔
620
            sensor["meter_location"] = {"value": site["Meter_Location"]}
6✔
621
        if "Mode" in site:
6✔
622
            sensor["meter_mode"] = {"value": site["Mode"]}
6✔
623
        if "P_Akku" in site:
6✔
624
            sensor["power_battery"] = {"value": site["P_Akku"], "unit": WATT}
6✔
625
        if "P_Grid" in site:
6✔
626
            sensor["power_grid"] = {"value": site["P_Grid"], "unit": WATT}
6✔
627
        if "P_Load" in site:
6✔
628
            sensor["power_load"] = {"value": site["P_Load"], "unit": WATT}
6✔
629
        if "P_PV" in site:
6✔
630
            sensor["power_photovoltaics"] = {"value": site["P_PV"], "unit": WATT}
6✔
631
        if "rel_Autonomy" in site:
6✔
632
            sensor["relative_autonomy"] = {
×
633
                "value": site["rel_Autonomy"],
634
                "unit": PERCENT,
635
            }
636
        if "rel_SelfConsumption" in site:
6✔
637
            sensor["relative_self_consumption"] = {
×
638
                "value": site["rel_SelfConsumption"],
639
                "unit": PERCENT,
640
            }
641

642
        return sensor
6✔
643

644
    @staticmethod
6✔
645
    def _system_meter_data(data: Dict[str, Any]) -> Dict[str, Any]:
6✔
646
        _LOGGER.debug("Converting system meter data: '{}'".format(data))
6✔
647

648
        sensor: Dict[str, Dict[str, Dict[str, Any]]] = {"meters": {}}
6✔
649

650
        for device_id, device_data in data.items():
6✔
651
            sensor["meters"][device_id] = Fronius._device_meter_data(device_data)
6✔
652

653
        return sensor
6✔
654

655
    @staticmethod
6✔
656
    def _system_inverter_data(data: Dict[str, Any]) -> Dict[str, Any]:
6✔
657
        _LOGGER.debug("Converting system inverter data: '{}'".format(data))
6✔
658
        sensor: Dict[str, Dict[str, Any]] = {}
6✔
659

660
        sensor["energy_day"] = {"value": 0, "unit": WATT_HOUR}
6✔
661
        sensor["energy_total"] = {"value": 0, "unit": WATT_HOUR}
6✔
662
        sensor["energy_year"] = {"value": 0, "unit": WATT_HOUR}
6✔
663
        sensor["power_ac"] = {"value": 0, "unit": WATT}
6✔
664

665
        sensor["inverters"] = {}
6✔
666

667
        if "DAY_ENERGY" in data:
6✔
668
            for i in data["DAY_ENERGY"]["Values"]:
6✔
669
                sensor["inverters"][i] = {}
6✔
670
                value = data["DAY_ENERGY"]["Values"][i]
6✔
671
                sensor["inverters"][i]["energy_day"] = {
6✔
672
                    "value": value,
673
                    "unit": data["DAY_ENERGY"]["Unit"],
674
                }
675
                sensor["energy_day"]["value"] += value or 0
6✔
676
        if "TOTAL_ENERGY" in data:
6✔
677
            for i in data["TOTAL_ENERGY"]["Values"]:
6✔
678
                value = data["TOTAL_ENERGY"]["Values"][i]
6✔
679
                sensor["inverters"][i]["energy_total"] = {
6✔
680
                    "value": value,
681
                    "unit": data["TOTAL_ENERGY"]["Unit"],
682
                }
683
                sensor["energy_total"]["value"] += value or 0
6✔
684
        if "YEAR_ENERGY" in data:
6✔
685
            for i in data["YEAR_ENERGY"]["Values"]:
6✔
686
                value = data["YEAR_ENERGY"]["Values"][i]
6✔
687
                sensor["inverters"][i]["energy_year"] = {
6✔
688
                    "value": value,
689
                    "unit": data["YEAR_ENERGY"]["Unit"],
690
                }
691
                sensor["energy_year"]["value"] += value or 0
6✔
692
        if "PAC" in data:
6✔
693
            for i in data["PAC"]["Values"]:
6✔
694
                value = data["PAC"]["Values"][i]
6✔
695
                sensor["inverters"][i]["power_ac"] = {
6✔
696
                    "value": value,
697
                    "unit": data["PAC"]["Unit"],
698
                }
699
                sensor["power_ac"]["value"] += value or 0
6✔
700

701
        return sensor
6✔
702

703
    @staticmethod
6✔
704
    def _device_ohmpilot_data(data: Dict[str, Any]) -> Dict[str, Any]:
6✔
705
        _LOGGER.debug("Converting ohmpilot data from '{}'".format(data))
6✔
706
        device = {}
6✔
707

708
        if "CodeOfError" in data:
6✔
709
            device["error_code"] = {"value": data["CodeOfError"]}
6✔
710

711
        if "CodeOfState" in data:
6✔
712
            state_code = data["CodeOfState"]
6✔
713
            device["state_code"] = {"value": state_code}
6✔
714
            device["state_message"] = {
6✔
715
                "value": OHMPILOT_STATE_CODES.get(state_code, "Unknown")
716
            }
717

718
        if "Details" in data:
6✔
719
            device["hardware"] = {"value": data["Details"]["Hardware"]}
6✔
720
            device["manufacturer"] = {"value": data["Details"]["Manufacturer"]}
6✔
721
            device["model"] = {"value": data["Details"]["Model"]}
6✔
722
            device["serial"] = {"value": data["Details"]["Serial"]}
6✔
723
            device["software"] = {"value": data["Details"]["Software"]}
6✔
724

725
        if "EnergyReal_WAC_Sum_Consumed" in data:
6✔
726
            device["energy_real_ac_consumed"] = {
6✔
727
                "value": data["EnergyReal_WAC_Sum_Consumed"],
728
                "unit": WATT_HOUR,
729
            }
730

731
        if "PowerReal_PAC_Sum" in data:
6✔
732
            device["power_real_ac"] = {"value": data["PowerReal_PAC_Sum"], "unit": WATT}
6✔
733

734
        if "Temperature_Channel_1" in data:
6✔
735
            device["temperature_channel_1"] = {
6✔
736
                "value": data["Temperature_Channel_1"],
737
                "unit": DEGREE_CELSIUS,
738
            }
739

740
        return device
6✔
741

742
    @staticmethod
6✔
743
    def _system_ohmpilot_data(data: Dict[str, Any]) -> Dict[str, Any]:
6✔
744
        _LOGGER.debug("Converting system ohmpilot data: '{}'".format(data))
6✔
745
        sensor: Dict[str, Dict[str, Dict[str, Any]]] = {"ohmpilots": {}}
6✔
746

747
        for device_id, device_data in data.items():
6✔
748
            sensor["ohmpilots"][device_id] = Fronius._device_ohmpilot_data(device_data)
6✔
749

750
        return sensor
6✔
751

752
    @staticmethod
6✔
753
    def _device_meter_data(data: Dict[str, Any]) -> Dict[str, Any]:
6✔
754
        _LOGGER.debug("Converting meter data: '{}'".format(data))
6✔
755

756
        meter = {}
6✔
757

758
        if "Current_AC_Phase_1" in data:
6✔
759
            meter["current_ac_phase_1"] = {
×
760
                "value": data["Current_AC_Phase_1"],
761
                "unit": AMPERE,
762
            }
763
        if "ACBRIDGE_CURRENT_ACTIVE_MEAN_01_F32" in data:
6✔
764
            meter["current_ac_phase_1"] = {
×
765
                "value": data["ACBRIDGE_CURRENT_ACTIVE_MEAN_01_F32"],
766
                "unit": AMPERE,
767
            }
768
        if "Current_AC_Phase_2" in data:
6✔
769
            meter["current_ac_phase_2"] = {
×
770
                "value": data["Current_AC_Phase_2"],
771
                "unit": AMPERE,
772
            }
773
        if "ACBRIDGE_CURRENT_ACTIVE_MEAN_02_F32" in data:
6✔
774
            meter["current_ac_phase_2"] = {
×
775
                "value": data["ACBRIDGE_CURRENT_ACTIVE_MEAN_02_F32"],
776
                "unit": AMPERE,
777
            }
778
        if "Current_AC_Phase_3" in data:
6✔
779
            meter["current_ac_phase_3"] = {
×
780
                "value": data["Current_AC_Phase_3"],
781
                "unit": AMPERE,
782
            }
783
        if "ACBRIDGE_CURRENT_ACTIVE_MEAN_03_F32" in data:
6✔
784
            meter["current_ac_phase_3"] = {
×
785
                "value": data["ACBRIDGE_CURRENT_ACTIVE_MEAN_03_F32"],
786
                "unit": AMPERE,
787
            }
788
        if "EnergyReactive_VArAC_Sum_Consumed" in data:
6✔
789
            meter["energy_reactive_ac_consumed"] = {
×
790
                "value": data["EnergyReactive_VArAC_Sum_Consumed"],
791
                "unit": VOLTAMPEREREACTIVE_HOUR,
792
            }
793
        if "EnergyReactive_VArAC_Sum_Produced" in data:
6✔
794
            meter["energy_reactive_ac_produced"] = {
×
795
                "value": data["EnergyReactive_VArAC_Sum_Produced"],
796
                "unit": VOLTAMPEREREACTIVE_HOUR,
797
            }
798
        if "EnergyReal_WAC_Minus_Absolute" in data:
6✔
799
            meter["energy_real_ac_minus"] = {
×
800
                "value": data["EnergyReal_WAC_Minus_Absolute"],
801
                "unit": WATT_HOUR,
802
            }
803
        if "EnergyReal_WAC_Plus_Absolute" in data:
6✔
804
            meter["energy_real_ac_plus"] = {
×
805
                "value": data["EnergyReal_WAC_Plus_Absolute"],
806
                "unit": WATT_HOUR,
807
            }
808
        if "EnergyReal_WAC_Sum_Consumed" in data:
6✔
809
            meter["energy_real_consumed"] = {
×
810
                "value": data["EnergyReal_WAC_Sum_Consumed"],
811
                "unit": WATT_HOUR,
812
            }
813
        if "SMARTMETER_ENERGYACTIVE_CONSUMED_SUM_F64" in data:
6✔
814
            meter["energy_real_consumed"] = {
×
815
                "value": data["SMARTMETER_ENERGYACTIVE_CONSUMED_SUM_F64"],
816
                "unit": WATT_HOUR,
817
            }
818
        if "EnergyReal_WAC_Sum_Produced" in data:
6✔
819
            meter["energy_real_produced"] = {
×
820
                "value": data["EnergyReal_WAC_Sum_Produced"],
821
                "unit": WATT_HOUR,
822
            }
823
        if "SMARTMETER_ENERGYACTIVE_PRODUCED_SUM_F64" in data:
6✔
824
            meter["energy_real_produced"] = {
×
825
                "value": data["SMARTMETER_ENERGYACTIVE_PRODUCED_SUM_F64"],
826
                "unit": WATT_HOUR,
827
            }
828
        if "Frequency_Phase_Average" in data:
6✔
829
            meter["frequency_phase_average"] = {
×
830
                "value": data["Frequency_Phase_Average"],
831
                "unit": HERTZ,
832
            }
833
        if "PowerApparent_S_Phase_1" in data:
6✔
834
            meter["power_apparent_phase_1"] = {
×
835
                "value": data["PowerApparent_S_Phase_1"],
836
                "unit": VOLTAMPERE,
837
            }
838
        if "PowerApparent_S_Phase_2" in data:
6✔
839
            meter["power_apparent_phase_2"] = {
×
840
                "value": data["PowerApparent_S_Phase_2"],
841
                "unit": VOLTAMPERE,
842
            }
843
        if "PowerApparent_S_Phase_3" in data:
6✔
844
            meter["power_apparent_phase_3"] = {
×
845
                "value": data["PowerApparent_S_Phase_3"],
846
                "unit": VOLTAMPERE,
847
            }
848
        if "PowerApparent_S_Sum" in data:
6✔
849
            meter["power_apparent"] = {
×
850
                "value": data["PowerApparent_S_Sum"],
851
                "unit": VOLTAMPERE,
852
            }
853
        if "PowerFactor_Phase_1" in data:
6✔
854
            meter["power_factor_phase_1"] = {
×
855
                "value": data["PowerFactor_Phase_1"],
856
            }
857
        if "PowerFactor_Phase_2" in data:
6✔
858
            meter["power_factor_phase_2"] = {
×
859
                "value": data["PowerFactor_Phase_2"],
860
            }
861
        if "PowerFactor_Phase_3" in data:
6✔
862
            meter["power_factor_phase_3"] = {
×
863
                "value": data["PowerFactor_Phase_3"],
864
            }
865
        if "PowerFactor_Sum" in data:
6✔
866
            meter["power_factor"] = {"value": data["PowerFactor_Sum"]}
×
867
        if "PowerReactive_Q_Phase_1" in data:
6✔
868
            meter["power_reactive_phase_1"] = {
×
869
                "value": data["PowerReactive_Q_Phase_1"],
870
                "unit": VOLTAMPEREREACTIVE,
871
            }
872
        if "PowerReactive_Q_Phase_2" in data:
6✔
873
            meter["power_reactive_phase_2"] = {
×
874
                "value": data["PowerReactive_Q_Phase_2"],
875
                "unit": VOLTAMPEREREACTIVE,
876
            }
877
        if "PowerReactive_Q_Phase_3" in data:
6✔
878
            meter["power_reactive_phase_3"] = {
×
879
                "value": data["PowerReactive_Q_Phase_3"],
880
                "unit": VOLTAMPEREREACTIVE,
881
            }
882
        if "PowerReactive_Q_Sum" in data:
6✔
883
            meter["power_reactive"] = {
×
884
                "value": data["PowerReactive_Q_Sum"],
885
                "unit": VOLTAMPEREREACTIVE,
886
            }
887
        if "PowerReal_P_Phase_1" in data:
6✔
888
            meter["power_real_phase_1"] = {
×
889
                "value": data["PowerReal_P_Phase_1"],
890
                "unit": WATT,
891
            }
892
        if "SMARTMETER_POWERACTIVE_01_F64" in data:
6✔
893
            meter["power_real_phase_1"] = {
×
894
                "value": data["SMARTMETER_POWERACTIVE_01_F64"],
895
                "unit": WATT,
896
            }
897
        if "PowerReal_P_Phase_2" in data:
6✔
898
            meter["power_real_phase_2"] = {
×
899
                "value": data["PowerReal_P_Phase_2"],
900
                "unit": WATT,
901
            }
902
        if "SMARTMETER_POWERACTIVE_02_F64" in data:
6✔
903
            meter["power_real_phase_2"] = {
×
904
                "value": data["SMARTMETER_POWERACTIVE_02_F64"],
905
                "unit": WATT,
906
            }
907
        if "PowerReal_P_Phase_3" in data:
6✔
908
            meter["power_real_phase_3"] = {
×
909
                "value": data["PowerReal_P_Phase_3"],
910
                "unit": WATT,
911
            }
912
        if "SMARTMETER_POWERACTIVE_03_F64" in data:
6✔
913
            meter["power_real_phase_3"] = {
×
914
                "value": data["SMARTMETER_POWERACTIVE_03_F64"],
915
                "unit": WATT,
916
            }
917
        if "PowerReal_P_Sum" in data:
6✔
918
            meter["power_real"] = {"value": data["PowerReal_P_Sum"], "unit": WATT}
6✔
919
        if "Voltage_AC_Phase_1" in data:
6✔
920
            meter["voltage_ac_phase_1"] = {
×
921
                "value": data["Voltage_AC_Phase_1"],
922
                "unit": VOLT,
923
            }
924
        if "Voltage_AC_Phase_2" in data:
6✔
925
            meter["voltage_ac_phase_2"] = {
×
926
                "value": data["Voltage_AC_Phase_2"],
927
                "unit": VOLT,
928
            }
929
        if "Voltage_AC_Phase_3" in data:
6✔
930
            meter["voltage_ac_phase_3"] = {
×
931
                "value": data["Voltage_AC_Phase_3"],
932
                "unit": VOLT,
933
            }
934
        if "Voltage_AC_PhaseToPhase_12" in data:
6✔
935
            meter["voltage_ac_phase_to_phase_12"] = {
×
936
                "value": data["Voltage_AC_PhaseToPhase_12"],
937
                "unit": VOLT,
938
            }
939
        if "Voltage_AC_PhaseToPhase_23" in data:
6✔
940
            meter["voltage_ac_phase_to_phase_23"] = {
×
941
                "value": data["Voltage_AC_PhaseToPhase_23"],
942
                "unit": VOLT,
943
            }
944
        if "Voltage_AC_PhaseToPhase_31" in data:
6✔
945
            meter["voltage_ac_phase_to_phase_31"] = {
×
946
                "value": data["Voltage_AC_PhaseToPhase_31"],
947
                "unit": VOLT,
948
            }
949
        if "Meter_Location_Current" in data:
6✔
950
            meter["meter_location"] = {"value": data["Meter_Location_Current"]}
6✔
951
        if "Enable" in data:
6✔
952
            meter["enable"] = {"value": data["Enable"]}
6✔
953
        if "Visible" in data:
6✔
954
            meter["visible"] = {"value": data["Visible"]}
6✔
955
        if "Details" in data:
6✔
956
            meter["manufacturer"] = {"value": data["Details"]["Manufacturer"]}
6✔
957
            meter["model"] = {"value": data["Details"]["Model"]}
6✔
958
            meter["serial"] = {"value": data["Details"]["Serial"]}
6✔
959

960
        return meter
6✔
961

962
    @staticmethod
6✔
963
    def _device_storage_data(data: Dict[str, Any]) -> Dict[str, Any]:
6✔
964
        _LOGGER.debug("Converting storage data from '{}'".format(data))
6✔
965
        sensor = {}
6✔
966

967
        if "Controller" in data:
6✔
968
            controller = Fronius._controller_data(data["Controller"])
6✔
969
            sensor.update(controller)
6✔
970

971
        if "Modules" in data:
6✔
972
            sensor["modules"] = {}
6✔
973
            module_count = 0
6✔
974

975
            for module in data["Modules"]:
6✔
976
                sensor["modules"][module_count] = Fronius._module_data(module)
×
977
                module_count += 1
×
978

979
        return sensor
6✔
980

981
    @staticmethod
6✔
982
    def _system_storage_data(data: Dict[str, Any]) -> Dict[str, Any]:
6✔
983
        _LOGGER.debug("Converting system storage data: '{}'".format(data))
6✔
984

985
        sensor: Dict[str, Dict[str, Dict[str, Any]]] = {"storages": {}}
6✔
986

987
        for device_id, device_data in data.items():
6✔
988
            sensor["storages"][device_id] = Fronius._device_storage_data(device_data)
6✔
989

990
        return sensor
6✔
991

992
    @staticmethod
6✔
993
    def _device_inverter_data(data: Dict[str, Any]) -> Dict[str, Any]:
6✔
994
        _LOGGER.debug("Converting inverter data from '{}'".format(data))
6✔
995
        sensor = {}
6✔
996

997
        if "DAY_ENERGY" in data:
6✔
998
            sensor["energy_day"] = {
6✔
999
                "value": data["DAY_ENERGY"]["Value"],
1000
                "unit": data["DAY_ENERGY"]["Unit"],
1001
            }
1002
        if "TOTAL_ENERGY" in data:
6✔
1003
            sensor["energy_total"] = {
6✔
1004
                "value": data["TOTAL_ENERGY"]["Value"],
1005
                "unit": data["TOTAL_ENERGY"]["Unit"],
1006
            }
1007
        if "YEAR_ENERGY" in data:
6✔
1008
            sensor["energy_year"] = {
6✔
1009
                "value": data["YEAR_ENERGY"]["Value"],
1010
                "unit": data["YEAR_ENERGY"]["Unit"],
1011
            }
1012
        if "FAC" in data:
6✔
1013
            sensor["frequency_ac"] = {
6✔
1014
                "value": data["FAC"]["Value"],
1015
                "unit": data["FAC"]["Unit"],
1016
            }
1017
        if "IAC" in data:
6✔
1018
            sensor["current_ac"] = {
6✔
1019
                "value": data["IAC"]["Value"],
1020
                "unit": data["IAC"]["Unit"],
1021
            }
1022
        if "IDC" in data:
6✔
1023
            sensor["current_dc"] = {
6✔
1024
                "value": data["IDC"]["Value"],
1025
                "unit": data["IDC"]["Unit"],
1026
            }
1027
        for i in range(2, 10):
6✔
1028
            if f"IDC_{i}" in data:
6✔
1029
                sensor[f"current_dc_{i}"] = {
×
1030
                    "value": data[f"IDC_{i}"]["Value"],
1031
                    "unit": data[f"IDC_{i}"]["Unit"],
1032
                }
1033
        if "PAC" in data:
6✔
1034
            sensor["power_ac"] = {
6✔
1035
                "value": data["PAC"]["Value"],
1036
                "unit": data["PAC"]["Unit"],
1037
            }
1038
        if "UAC" in data:
6✔
1039
            sensor["voltage_ac"] = {
6✔
1040
                "value": data["UAC"]["Value"],
1041
                "unit": data["UAC"]["Unit"],
1042
            }
1043
        if "UDC" in data:
6✔
1044
            sensor["voltage_dc"] = {
6✔
1045
                "value": data["UDC"]["Value"],
1046
                "unit": data["UDC"]["Unit"],
1047
            }
1048
        for i in range(2, 10):
6✔
1049
            if f"UDC_{i}" in data:
6✔
1050
                sensor[f"voltage_dc_{i}"] = {
×
1051
                    "value": data[f"UDC_{i}"]["Value"],
1052
                    "unit": data[f"UDC_{i}"]["Unit"],
1053
                }
1054
        if "DeviceStatus" in data:
6✔
1055
            if "InverterState" in data["DeviceStatus"]:
6✔
1056
                sensor["inverter_state"] = {
×
1057
                    "value": data["DeviceStatus"]["InverterState"]
1058
                }
1059
            if "ErrorCode" in data["DeviceStatus"]:
6✔
1060
                sensor["error_code"] = {"value": data["DeviceStatus"]["ErrorCode"]}
6✔
1061
            if "StatusCode" in data["DeviceStatus"]:
6✔
1062
                sensor["status_code"] = {"value": data["DeviceStatus"]["StatusCode"]}
6✔
1063
            if "LEDState" in data["DeviceStatus"]:
6✔
1064
                sensor["led_state"] = {"value": data["DeviceStatus"]["LEDState"]}
6✔
1065
            if "LEDColor" in data["DeviceStatus"]:
6✔
1066
                sensor["led_color"] = {"value": data["DeviceStatus"]["LEDColor"]}
6✔
1067

1068
        return sensor
6✔
1069

1070
    @staticmethod
6✔
1071
    def _device_inverter_3p_data(data):
6✔
1072
        _LOGGER.debug("Converting inverter 3p data from '{}'".format(data))
6✔
1073
        sensor = {}
6✔
1074
        if "IAC_L1" in data:
6✔
1075
            sensor["current_ac_phase_1"] = {
6✔
1076
                "value": data["IAC_L1"]["Value"],
1077
                "unit": data["IAC_L1"]["Unit"],
1078
            }
1079
        if "IAC_L2" in data:
6✔
1080
            sensor["current_ac_phase_2"] = {
6✔
1081
                "value": data["IAC_L2"]["Value"],
1082
                "unit": data["IAC_L2"]["Unit"],
1083
            }
1084
        if "IAC_L3" in data:
6✔
1085
            sensor["current_ac_phase_3"] = {
6✔
1086
                "value": data["IAC_L3"]["Value"],
1087
                "unit": data["IAC_L3"]["Unit"],
1088
            }
1089
        if "UAC_L1" in data:
6✔
1090
            sensor["voltage_ac_phase_1"] = {
6✔
1091
                "value": data["UAC_L1"]["Value"],
1092
                "unit": data["UAC_L1"]["Unit"],
1093
            }
1094
        if "UAC_L2" in data:
6✔
1095
            sensor["voltage_ac_phase_2"] = {
6✔
1096
                "value": data["UAC_L2"]["Value"],
1097
                "unit": data["UAC_L2"]["Unit"],
1098
            }
1099
        if "UAC_L3" in data:
6✔
1100
            sensor["voltage_ac_phase_3"] = {
6✔
1101
                "value": data["UAC_L3"]["Value"],
1102
                "unit": data["UAC_L3"]["Unit"],
1103
            }
1104
        return sensor
6✔
1105

1106
    @staticmethod
6✔
1107
    def _controller_data(data: Dict[str, Any]) -> Dict[str, Any]:
6✔
1108
        controller = {}
6✔
1109

1110
        if "Capacity_Maximum" in data:
6✔
1111
            controller["capacity_maximum"] = {
6✔
1112
                "value": data["Capacity_Maximum"],
1113
                "unit": "Ah",
1114
            }
1115
        if "DesignedCapacity" in data:
6✔
1116
            controller["capacity_designed"] = {
6✔
1117
                "value": data["DesignedCapacity"],
1118
                "unit": "Ah",
1119
            }
1120
        if "Current_DC" in data:
6✔
1121
            controller["current_dc"] = {"value": data["Current_DC"], "unit": AMPERE}
×
1122
        if "Voltage_DC" in data:
6✔
1123
            controller["voltage_dc"] = {"value": data["Voltage_DC"], "unit": VOLT}
6✔
1124
        if "Voltage_DC_Maximum_Cell" in data:
6✔
1125
            controller["voltage_dc_maximum_cell"] = {
×
1126
                "value": data["Voltage_DC_Maximum_Cell"],
1127
                "unit": VOLT,
1128
            }
1129
        if "Voltage_DC_Minimum_Cell" in data:
6✔
1130
            controller["voltage_dc_minimum_cell"] = {
×
1131
                "value": data["Voltage_DC_Minimum_Cell"],
1132
                "unit": VOLT,
1133
            }
1134
        if "StateOfCharge_Relative" in data:
6✔
1135
            controller["state_of_charge"] = {
6✔
1136
                "value": data["StateOfCharge_Relative"],
1137
                "unit": PERCENT,
1138
            }
1139
        if "Temperature_Cell" in data:
6✔
1140
            controller["temperature_cell"] = {
6✔
1141
                "value": data["Temperature_Cell"],
1142
                "unit": DEGREE_CELSIUS,
1143
            }
1144
        if "Enable" in data:
6✔
1145
            controller["enable"] = {"value": data["Enable"]}
6✔
1146
        if "Details" in data:
6✔
1147
            controller["manufacturer"] = {"value": data["Details"]["Manufacturer"]}
6✔
1148
            controller["model"] = {"value": data["Details"]["Model"]}
6✔
1149
            controller["serial"] = {"value": data["Details"]["Serial"]}
6✔
1150

1151
        return controller
6✔
1152

1153
    @staticmethod
6✔
1154
    def _module_data(data: Dict[str, Any]) -> Dict[str, Any]:
6✔
1155
        module = {}
×
1156

1157
        if "Capacity_Maximum" in data:
×
1158
            module["capacity_maximum"] = {
×
1159
                "value": data["Capacity_Maximum"],
1160
                "unit": "Ah",
1161
            }
1162
        if "DesignedCapacity" in data:
×
1163
            module["capacity_designed"] = {
×
1164
                "value": data["DesignedCapacity"],
1165
                "unit": "Ah",
1166
            }
1167
        if "Current_DC" in data:
×
1168
            module["current_dc"] = {"value": data["Current_DC"], "unit": AMPERE}
×
1169
        if "Voltage_DC" in data:
×
1170
            module["voltage_dc"] = {"value": data["Voltage_DC"], "unit": VOLT}
×
1171
        if "Voltage_DC_Maximum_Cell" in data:
×
1172
            module["voltage_dc_maximum_cell"] = {
×
1173
                "value": data["Voltage_DC_Maximum_Cell"],
1174
                "unit": VOLT,
1175
            }
1176
        if "Voltage_DC_Minimum_Cell" in data:
×
1177
            module["voltage_dc_minimum_cell"] = {
×
1178
                "value": data["Voltage_DC_Minimum_Cell"],
1179
                "unit": VOLT,
1180
            }
1181
        if "StateOfCharge_Relative" in data:
×
1182
            module["state_of_charge"] = {
×
1183
                "value": data["StateOfCharge_Relative"],
1184
                "unit": PERCENT,
1185
            }
1186
        if "Temperature_Cell" in data:
×
1187
            module["temperature_cell"] = {
×
1188
                "value": data["Temperature_Cell"],
1189
                "unit": DEGREE_CELSIUS,
1190
            }
1191
        if "Temperature_Cell_Maximum" in data:
×
1192
            module["temperature_cell_maximum"] = {
×
1193
                "value": data["Temperature_Cell_Maximum"],
1194
                "unit": DEGREE_CELSIUS,
1195
            }
1196
        if "Temperature_Cell_Minimum" in data:
×
1197
            module["temperature_cell_minimum"] = {
×
1198
                "value": data["Temperature_Cell_Minimum"],
1199
                "unit": DEGREE_CELSIUS,
1200
            }
1201
        if "CycleCount_BatteryCell" in data:
×
1202
            module["cycle_count_cell"] = {"value": data["CycleCount_BatteryCell"]}
×
1203
        if "Status_BatteryCell" in data:
×
1204
            module["status_cell"] = {"value": data["Status_BatteryCell"]}
×
1205
        if "Enable" in data:
×
1206
            module["enable"] = {"value": data["Enable"]}
×
1207
        if "Details" in data:
×
1208
            module["manufacturer"] = {"value": data["Details"]["Manufacturer"]}
×
1209
            module["model"] = {"value": data["Details"]["Model"]}
×
1210
            module["serial"] = {"value": data["Details"]["Serial"]}
×
1211

1212
        return module
×
1213

1214
    @staticmethod
6✔
1215
    def _system_active_device_info(data: Dict[str, Any]) -> Dict[str, Any]:
6✔
1216
        _LOGGER.debug("Converting system active device data: '{}'".format(data))
6✔
1217
        sensor = {}
6✔
1218

1219
        if "Inverter" in data:
6✔
1220
            inverters = []
6✔
1221
            for device_id, device in data["Inverter"].items():
6✔
1222
                inverter = {"device_id": device_id, "device_type": device["DT"]}
6✔
1223
                if "Serial" in device:
6✔
1224
                    inverter["serial_number"] = device["Serial"]
6✔
1225
                inverters.append(inverter)
6✔
1226
            sensor["inverters"] = inverters
6✔
1227

1228
        if "Meter" in data:
6✔
1229
            meters = []
6✔
1230
            for device_id, device in data["Meter"].items():
6✔
1231
                meter = {"device_id": device_id}
6✔
1232
                if "Serial" in device:
6✔
1233
                    meter["serial_number"] = device["Serial"]
6✔
1234
                meters.append(meter)
6✔
1235
            sensor["meters"] = meters
6✔
1236

1237
        if "Ohmpilot" in data:
6✔
1238
            ohmpilots = []
6✔
1239
            for device_id, device in data["Ohmpilot"].items():
6✔
1240
                ohmpilot = {"device_id": device_id}
×
1241
                if "Serial" in device:
×
1242
                    ohmpilot["serial_number"] = device["Serial"]
×
1243
                ohmpilots.append(ohmpilot)
×
1244
            sensor["ohmpilots"] = ohmpilots
6✔
1245

1246
        if "SensorCard" in data:
6✔
1247
            sensor_cards = []
6✔
1248
            for device_id, device in data["SensorCard"].items():
6✔
1249
                sensor_card = {"device_id": device_id, "device_type": device["DT"]}
×
1250
                if "Serial" in device:
×
1251
                    sensor_card["serial_number"] = device["Serial"]
×
1252
                sensor_card["channel_names"] = list(
×
1253
                    map(lambda x: x.lower().replace(" ", "_"), device["ChannelNames"])
1254
                )
1255
                sensor_cards.append(sensor_card)
×
1256
            sensor["sensor_cards"] = sensor_cards
6✔
1257

1258
        if "Storage" in data:
6✔
1259
            storages = []
6✔
1260
            for device_id, device in data["Storage"].items():
6✔
1261
                storage = {"device_id": device_id}
×
1262
                if "Serial" in device:
×
1263
                    storage["serial_number"] = device["Serial"]
×
1264
                storages.append(storage)
×
1265
            sensor["storages"] = storages
6✔
1266

1267
        if "StringControl" in data:
6✔
1268
            string_controls = []
6✔
1269
            for device_id, device in data["StringControl"].items():
6✔
1270
                string_control = {"device_id": device_id}
×
1271
                if "Serial" in device:
×
1272
                    string_control["serial_number"] = device["Serial"]
×
1273
                string_controls.append(string_control)
×
1274
            sensor["string_controls"] = string_controls
6✔
1275

1276
        return sensor
6✔
1277

1278
    @staticmethod
6✔
1279
    def _inverter_info(data: Dict[str, Any]) -> Dict[str, List[Dict[str, Any]]]:
6✔
1280
        """Parse inverter info."""
1281
        _LOGGER.debug("Converting inverter info: '{}'".format(data))
6✔
1282
        inverters = []
6✔
1283
        for inverter_index, inverter_info in data.items():
6✔
1284
            inverter = {
6✔
1285
                "device_id": {"value": inverter_index},
1286
                "device_type": {"value": inverter_info["DT"]},
1287
                "pv_power": {"value": inverter_info["PVPower"], "unit": WATT},
1288
                "status_code": {"value": inverter_info["StatusCode"]},
1289
                "unique_id": {"value": inverter_info["UniqueID"]},
1290
            }
1291
            if inverter_info["DT"] in INVERTER_DEVICE_TYPE:
6✔
1292
                # add manufacturer and model if known
1293
                inverter["device_type"].update(
6✔
1294
                    INVERTER_DEVICE_TYPE[inverter_info["DT"]]
1295
                )
1296
            # "CustomName" not available on API V0 so default to ""
1297
            # html escaped by V1 Snap-In, UTF-8 by V1 Gen24
1298
            if "CustomName" in inverter_info:
6✔
1299
                inverter["custom_name"] = {
6✔
1300
                    "value": unescape(inverter_info["CustomName"])
1301
                }
1302
            # "ErrorCode" not in V1-Gen24
1303
            if "ErrorCode" in inverter_info:
6✔
1304
                inverter["error_code"] = {"value": inverter_info["ErrorCode"]}
6✔
1305
            # "Show" not in V0
1306
            if "Show" in inverter_info:
6✔
1307
                inverter["show"] = {"value": inverter_info["Show"]}
6✔
1308
            inverters.append(inverter)
6✔
1309
        return {"inverters": inverters}
6✔
1310

1311
    @staticmethod
6✔
1312
    def _logger_info(data: Dict[str, Any]) -> Dict[str, Any]:
6✔
1313
        _LOGGER.debug("Converting Logger info: '{}'".format(data))
6✔
1314
        sensor = {}
6✔
1315

1316
        if "CO2Factor" in data and "CO2Unit" in data:
6✔
1317
            co2_unit = unescape(data["CO2Unit"])
6✔
1318
            sensor["co2_factor"] = {
6✔
1319
                "value": data["CO2Factor"],
1320
                "unit": f"{co2_unit}/kWh",
1321
            }
1322

1323
        if "CashCurrency" in data:
6✔
1324
            cash_currency = unescape(data["CashCurrency"])
6✔
1325
            if "CashFactor" in data:
6✔
1326
                sensor["cash_factor"] = {
6✔
1327
                    "value": data["CashFactor"],
1328
                    "unit": f"{cash_currency}/kWh",
1329
                }
1330
            if "DeliveryFactor" in data:
6✔
1331
                sensor["delivery_factor"] = {
6✔
1332
                    "value": data["DeliveryFactor"],
1333
                    "unit": f"{cash_currency}/kWh",
1334
                }
1335

1336
        if "HWVersion" in data:
6✔
1337
            sensor["hardware_version"] = {"value": data["HWVersion"]}
6✔
1338

1339
        if "SWVersion" in data:
6✔
1340
            sensor["software_version"] = {"value": data["SWVersion"]}
6✔
1341

1342
        if "PlatformID" in data:
6✔
1343
            sensor["hardware_platform"] = {"value": data["PlatformID"]}
6✔
1344

1345
        if "ProductID" in data:
6✔
1346
            sensor["product_type"] = {"value": data["ProductID"]}
6✔
1347

1348
        if "TimezoneLocation" in data:
6✔
1349
            sensor["time_zone_location"] = {"value": data["TimezoneLocation"]}
6✔
1350

1351
        if "TimezoneName" in data:
6✔
1352
            sensor["time_zone"] = {"value": data["TimezoneName"]}
6✔
1353

1354
        if "UTCOffset" in data:
6✔
1355
            sensor["utc_offset"] = {"value": data["UTCOffset"]}
6✔
1356

1357
        if "UniqueID" in data:
6✔
1358
            sensor["unique_identifier"] = {"value": data["UniqueID"]}
6✔
1359

1360
        return sensor
6✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc