• 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, ext_cb_conversion: Callable[[Dict[str, Any]], Dict[str, Any]] = None
395
    ) -> Dict[str, Any]:
396
        """
397
        Get the current power flow of a smart meter system.
1✔
398
        """
399
        cb = Fronius._system_power_flow
400
        if ext_cb_conversion is not None:
401
            cb = ext_cb_conversion
1✔
402
        return await self._current_data(cb, URL_POWER_FLOW, "current power flow")
403

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

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

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

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

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

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

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

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

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

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

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

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

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

580
        return sensor
1✔
581

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

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

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

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

640
        return sensor
641

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

1✔
646
        sensor: Dict[str, Dict[str, Dict[str, Any]]] = {"meters": {}}
647

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

651
        return sensor
1✔
652

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

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

663
        sensor["inverters"] = {}
1✔
664

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

×
699
        return sensor
700

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

706
        if "CodeOfError" in data:
707
            device["error_code"] = {"value": data["CodeOfError"]}
1✔
708

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

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

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

×
729
        if "PowerReal_PAC_Sum" in data:
730
            device["power_real_ac"] = {"value": data["PowerReal_PAC_Sum"], "unit": WATT}
731

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

1✔
738
        return device
×
739

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

745
        for device_id, device_data in data.items():
746
            sensor["ohmpilots"][device_id] = Fronius._device_ohmpilot_data(device_data)
747

1✔
748
        return sensor
×
749

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

×
754
        meter = {}
755

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

1✔
958
        return meter
959

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

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

969
        if "Modules" in data:
970
            sensor["modules"] = {}
971
            module_count = 0
1✔
972

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

1✔
977
        return sensor
1✔
978

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

1✔
983
        sensor: Dict[str, Dict[str, Dict[str, Any]]] = {"storages": {}}
1✔
984

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

1✔
988
        return sensor
989

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

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

×
1066
        return sensor
1067

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

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

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

1✔
1149
        return controller
1150

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

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

1✔
1210
        return module
1211

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

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

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

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

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

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

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

1274
        return sensor
1275

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

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

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

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

1334
        if "HWVersion" in data:
1335
            sensor["hardware_version"] = {"value": data["HWVersion"]}
1336

1337
        if "SWVersion" in data:
1338
            sensor["software_version"] = {"value": data["SWVersion"]}
1339

1340
        if "PlatformID" in data:
1341
            sensor["hardware_platform"] = {"value": data["PlatformID"]}
1342

1343
        if "ProductID" in data:
1344
            sensor["product_type"] = {"value": data["ProductID"]}
1345

1346
        if "TimezoneLocation" in data:
1347
            sensor["time_zone_location"] = {"value": data["TimezoneLocation"]}
1348

1349
        if "TimezoneName" in data:
1350
            sensor["time_zone"] = {"value": data["TimezoneName"]}
1351

1352
        if "UTCOffset" in data:
1353
            sensor["utc_offset"] = {"value": data["UTCOffset"]}
1354

1355
        if "UniqueID" in data:
1356
            sensor["unique_identifier"] = {"value": data["UniqueID"]}
1357

1358
        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