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

nielstron / pyfronius / #630144199

29 Jan 2025 08:50PM UTC coverage: 83.25%. First build
#630144199

Pull #19

travis-ci

Pull Request #19: add support for parsing a third DC line

2 of 4 new or added lines in 1 file covered. (50.0%)

835 of 1003 relevant lines covered (83.25%)

0.83 hits per line

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

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

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

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

15
import aiohttp
1✔
16

17
from .const import INVERTER_DEVICE_TYPE, OHMPILOT_STATE_CODES
1✔
18

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

31

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

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

39

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

45
URL_API_VERSION: Final = "solar_api/GetAPIVersion.cgi"
1✔
46
URL_POWER_FLOW: Final = {API_VERSION.V1: "GetPowerFlowRealtimeData.fcgi"}
1✔
47
URL_SYSTEM_METER: Final = {API_VERSION.V1: "GetMeterRealtimeData.cgi?Scope=System"}
1✔
48
URL_SYSTEM_INVERTER: Final = {
1✔
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"}
1✔
53
URL_SYSTEM_OHMPILOT: Final = {
1✔
54
    API_VERSION.V1: "GetOhmPilotRealtimeData.cgi?Scope=System"
1✔
55
}
56
URL_SYSTEM_STORAGE: Final = {API_VERSION.V1: "GetStorageRealtimeData.cgi?Scope=System"}
57
URL_DEVICE_METER: Final = {
1✔
58
    API_VERSION.V1: "GetMeterRealtimeData.cgi?Scope=Device&DeviceId={}"
1✔
59
}
60
URL_DEVICE_STORAGE: Final = {
61
    API_VERSION.V1: "GetStorageRealtimeData.cgi?Scope=Device&DeviceId={}"
1✔
62
}
63
URL_DEVICE_INVERTER_CUMULATIVE: Final = {
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
    ),
1✔
74
}
75
URL_DEVICE_INVERTER_COMMON: Final = {
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
    ),
1✔
86
}
87
URL_DEVICE_INVERTER_3P: Final = {
88
    API_VERSION.V1: (
1✔
89
        "GetInverterRealtimeData.cgi?Scope=Device&"
90
        "DeviceId={}&"
91
        "DataCollection=3PInverterData"
92
    ),
1✔
93
}
94
URL_ACTIVE_DEVICE_INFO_SYSTEM: Final = {
95
    API_VERSION.V1: "GetActiveDeviceInfo.cgi?DeviceClass=System"
96
}
97
URL_INVERTER_INFO: Final = {
1✔
98
    API_VERSION.V0: "GetInverterInfo.cgi",
99
    API_VERSION.V1: "GetInverterInfo.cgi",
100
}
101
URL_LOGGER_INFO: Final = {
102
    API_VERSION.V0: "GetLoggerInfo.cgi",
103
    API_VERSION.V1: "GetLoggerInfo.cgi",
104
}
105

106
HEADER_STATUS_CODES: Final = {
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",
1✔
116
    9: "LNParseError",
117
    10: "ConfigIOError",
118
    11: "NotSupported",
119
    12: "DeviceNotAvailable",
120
    255: "UnknownError",
121
}
122

1✔
123

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

1✔
130

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

137

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

143

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

149

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

×
153
    def __init__(
154
        self,
155
        endpoint: str,
156
        code: int,
157
        reason: Union[str, None] = None,
×
158
        response: Dict[str, Any] = {},
159
    ) -> None:
160
        """Instantiate exception."""
1✔
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:
171
    """
1✔
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)
1✔
178
        api_version  Version of Fronius API to use
1✔
179
    """
×
180

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

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

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

234
        return api_version, base_url
235

236
    async def _fetch_solar_api(
1✔
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:
244
            prev_api_version = self.api_version
245
            self.api_version, self.base_url = await self.fetch_api_version()
246
            if prev_api_version == API_VERSION.AUTO:
1✔
247
                _LOGGER.debug(
1✔
248
                    """using highest supported API version {}""".format(
1✔
249
                        self.api_version
250
                    )
251
                )
252
            if (
253
                prev_api_version != self.api_version
1✔
254
                and prev_api_version != API_VERSION.AUTO
1✔
255
            ):
256
                _LOGGER.warning(
1✔
257
                    (
1✔
258
                        """Unknown API version {} is not supported by host {},"""
1✔
259
                        """using highest supported API version {} instead"""
260
                    ).format(prev_api_version, self.url, self.api_version)
1✔
261
                )
262
        spec_url = spec.get(self.api_version)
263
        if spec_url is None:
264
            raise NotSupportedError(
265
                "API version {} does not support request of {} data".format(
266
                    self.api_version, spec_name
267
                )
268
            )
269
        if spec_formattings:
270
            spec_url = spec_url.format(*spec_formattings)
271

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

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

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

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

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

1✔
335
        return sensor
336

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

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

353
    async def _current_data(
1✔
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,
1✔
359
    ) -> Dict[str, Any]:
1✔
360
        sensor = {}
1✔
361
        try:
362
            res = await self._fetch_solar_api(spec, spec_name, *spec_formattings)
1✔
363
        except InvalidAnswerError:
1✔
364
            # except if Host returns 404
×
365
            raise NotSupportedError(
×
366
                "Device type {} not supported by the fronius device".format(spec_name)
367
            )
368

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

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

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

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

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

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

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

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

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

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

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

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

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

551
    async def inverter_info(
1✔
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
        """
1✔
558
        cb = Fronius._inverter_info
559
        if ext_cb_conversion is not None:
1✔
560
            cb = ext_cb_conversion
561
        return await self._current_data(cb, URL_INVERTER_INFO, "inverter info")
1✔
562

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

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

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

582
        return sensor
1✔
583

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

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

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

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

1✔
642
        return sensor
1✔
643

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

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

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

653
        return sensor
1✔
654

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

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

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

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

701
        return sensor
702

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

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

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

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

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

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

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

740
        return device
741

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

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

750
        return sensor
751

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

756
        meter = {}
757

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

960
        return meter
961

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

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

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

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

1✔
979
        return sensor
1✔
980

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

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

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

990
        return sensor
1✔
991

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

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

1068
        return sensor
1069

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

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

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

1✔
1151
        return controller
1✔
1152

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

×
1157
        if "Capacity_Maximum" in data:
1✔
1158
            module["capacity_maximum"] = {
1159
                "value": data["Capacity_Maximum"],
1✔
1160
                "unit": "Ah",
1161
            }
1✔
1162
        if "DesignedCapacity" in data:
1163
            module["capacity_designed"] = {
1164
                "value": data["DesignedCapacity"],
1✔
1165
                "unit": "Ah",
1✔
1166
            }
1✔
1167
        if "Current_DC" in data:
1✔
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,
1✔
1175
            }
1176
        if "Voltage_DC_Minimum_Cell" in data:
1✔
1177
            module["voltage_dc_minimum_cell"] = {
1178
                "value": data["Voltage_DC_Minimum_Cell"],
1179
                "unit": VOLT,
1180
            }
1181
        if "StateOfCharge_Relative" in data:
1✔
1182
            module["state_of_charge"] = {
1✔
1183
                "value": data["StateOfCharge_Relative"],
1184
                "unit": PERCENT,
1185
            }
1186
        if "Temperature_Cell" in data:
1✔
1187
            module["temperature_cell"] = {
1✔
1188
                "value": data["Temperature_Cell"],
1189
                "unit": DEGREE_CELSIUS,
1✔
1190
            }
1✔
1191
        if "Temperature_Cell_Maximum" in data:
1✔
1192
            module["temperature_cell_maximum"] = {
1✔
1193
                "value": data["Temperature_Cell_Maximum"],
1194
                "unit": DEGREE_CELSIUS,
1✔
1195
            }
1196
        if "Temperature_Cell_Minimum" in data:
1✔
1197
            module["temperature_cell_minimum"] = {
1✔
1198
                "value": data["Temperature_Cell_Minimum"],
1199
                "unit": DEGREE_CELSIUS,
1✔
1200
            }
1✔
1201
        if "CycleCount_BatteryCell" in data:
1✔
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"]}
1✔
1207
        if "Details" in data:
1✔
1208
            module["manufacturer"] = {"value": data["Details"]["Manufacturer"]}
1✔
1209
            module["model"] = {"value": data["Details"]["Model"]}
1✔
1210
            module["serial"] = {"value": data["Details"]["Serial"]}
1211

1212
        return module
1213

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

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

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

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

1246
        if "SensorCard" in data:
1247
            sensor_cards = []
1248
            for device_id, device in data["SensorCard"].items():
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
1257

1258
        if "Storage" in data:
1259
            storages = []
1260
            for device_id, device in data["Storage"].items():
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
1266

1267
        if "StringControl" in data:
1268
            string_controls = []
1269
            for device_id, device in data["StringControl"].items():
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
1275

1276
        return sensor
1277

1278
    @staticmethod
1279
    def _inverter_info(data: Dict[str, Any]) -> Dict[str, List[Dict[str, Any]]]:
1280
        """Parse inverter info."""
1281
        _LOGGER.debug("Converting inverter info: '{}'".format(data))
1282
        inverters = []
1283
        for inverter_index, inverter_info in data.items():
1284
            inverter = {
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:
1292
                # add manufacturer and model if known
1293
                inverter["device_type"].update(
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:
1299
                inverter["custom_name"] = {
1300
                    "value": unescape(inverter_info["CustomName"])
1301
                }
1302
            # "ErrorCode" not in V1-Gen24
1303
            if "ErrorCode" in inverter_info:
1304
                inverter["error_code"] = {"value": inverter_info["ErrorCode"]}
1305
            # "Show" not in V0
1306
            if "Show" in inverter_info:
1307
                inverter["show"] = {"value": inverter_info["Show"]}
1308
            inverters.append(inverter)
1309
        return {"inverters": inverters}
1310

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

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

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

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

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

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

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

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

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

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

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

1360
        return sensor
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