pyenphase 2.2.2__tar.gz → 2.3.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. {pyenphase-2.2.2 → pyenphase-2.3.0}/PKG-INFO +1 -1
  2. {pyenphase-2.2.2 → pyenphase-2.3.0}/pyproject.toml +3 -3
  3. {pyenphase-2.2.2 → pyenphase-2.3.0}/src/pyenphase/__init__.py +4 -0
  4. {pyenphase-2.2.2 → pyenphase-2.3.0}/src/pyenphase/const.py +2 -0
  5. pyenphase-2.3.0/src/pyenphase/models/c6combiner.py +59 -0
  6. pyenphase-2.3.0/src/pyenphase/models/collar.py +73 -0
  7. {pyenphase-2.2.2 → pyenphase-2.3.0}/src/pyenphase/models/dry_contacts.py +1 -0
  8. {pyenphase-2.2.2 → pyenphase-2.3.0}/src/pyenphase/models/envoy.py +6 -0
  9. {pyenphase-2.2.2 → pyenphase-2.3.0}/src/pyenphase/updaters/ensemble.py +26 -0
  10. {pyenphase-2.2.2 → pyenphase-2.3.0}/LICENSE +0 -0
  11. {pyenphase-2.2.2 → pyenphase-2.3.0}/README.md +0 -0
  12. {pyenphase-2.2.2 → pyenphase-2.3.0}/src/pyenphase/auth.py +0 -0
  13. {pyenphase-2.2.2 → pyenphase-2.3.0}/src/pyenphase/envoy.py +0 -0
  14. {pyenphase-2.2.2 → pyenphase-2.3.0}/src/pyenphase/exceptions.py +0 -0
  15. {pyenphase-2.2.2 → pyenphase-2.3.0}/src/pyenphase/firmware.py +0 -0
  16. {pyenphase-2.2.2 → pyenphase-2.3.0}/src/pyenphase/json.py +0 -0
  17. {pyenphase-2.2.2 → pyenphase-2.3.0}/src/pyenphase/models/__init__.py +0 -0
  18. {pyenphase-2.2.2 → pyenphase-2.3.0}/src/pyenphase/models/acb.py +0 -0
  19. {pyenphase-2.2.2 → pyenphase-2.3.0}/src/pyenphase/models/common.py +0 -0
  20. {pyenphase-2.2.2 → pyenphase-2.3.0}/src/pyenphase/models/encharge.py +0 -0
  21. {pyenphase-2.2.2 → pyenphase-2.3.0}/src/pyenphase/models/enpower.py +0 -0
  22. {pyenphase-2.2.2 → pyenphase-2.3.0}/src/pyenphase/models/home.py +0 -0
  23. {pyenphase-2.2.2 → pyenphase-2.3.0}/src/pyenphase/models/inverter.py +0 -0
  24. {pyenphase-2.2.2 → pyenphase-2.3.0}/src/pyenphase/models/meters.py +0 -0
  25. {pyenphase-2.2.2 → pyenphase-2.3.0}/src/pyenphase/models/storage_settings.py +0 -0
  26. {pyenphase-2.2.2 → pyenphase-2.3.0}/src/pyenphase/models/system_consumption.py +0 -0
  27. {pyenphase-2.2.2 → pyenphase-2.3.0}/src/pyenphase/models/system_production.py +0 -0
  28. {pyenphase-2.2.2 → pyenphase-2.3.0}/src/pyenphase/models/tariff.py +0 -0
  29. {pyenphase-2.2.2 → pyenphase-2.3.0}/src/pyenphase/py.typed +0 -0
  30. {pyenphase-2.2.2 → pyenphase-2.3.0}/src/pyenphase/ssl.py +0 -0
  31. {pyenphase-2.2.2 → pyenphase-2.3.0}/src/pyenphase/updaters/__init__.py +0 -0
  32. {pyenphase-2.2.2 → pyenphase-2.3.0}/src/pyenphase/updaters/api_v1_production.py +0 -0
  33. {pyenphase-2.2.2 → pyenphase-2.3.0}/src/pyenphase/updaters/api_v1_production_inverters.py +0 -0
  34. {pyenphase-2.2.2 → pyenphase-2.3.0}/src/pyenphase/updaters/base.py +0 -0
  35. {pyenphase-2.2.2 → pyenphase-2.3.0}/src/pyenphase/updaters/device_data_inverters.py +0 -0
  36. {pyenphase-2.2.2 → pyenphase-2.3.0}/src/pyenphase/updaters/generator.py +0 -0
  37. {pyenphase-2.2.2 → pyenphase-2.3.0}/src/pyenphase/updaters/meters.py +0 -0
  38. {pyenphase-2.2.2 → pyenphase-2.3.0}/src/pyenphase/updaters/production.py +0 -0
  39. {pyenphase-2.2.2 → pyenphase-2.3.0}/src/pyenphase/updaters/tariff.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: pyenphase
3
- Version: 2.2.2
3
+ Version: 2.3.0
4
4
  Summary: Library to control enphase envoy
5
5
  License: MIT
6
6
  Author: pyenphase
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "pyenphase"
3
- version = "2.2.2"
3
+ version = "2.3.0"
4
4
  description = "Library to control enphase envoy"
5
5
  authors = [{ name = "pyenphase", email = "cgarwood@gmail.com" }]
6
6
  license = "MIT"
@@ -10,7 +10,7 @@ dynamic = ["classifiers", "dependencies"]
10
10
 
11
11
  [tool.poetry]
12
12
  name = "pyenphase"
13
- version = "2.2.2"
13
+ version = "2.3.0"
14
14
  description = "Library to control enphase envoy"
15
15
  authors = ["pyenphase <cgarwood@gmail.com>"]
16
16
  classifiers = [
@@ -46,7 +46,7 @@ pytest = ">=7,<9"
46
46
  pytest-cov = ">=5,<7"
47
47
  types-orjson = "^3.6.2"
48
48
  aioresponses = ">=0.7.6"
49
- pytest-asyncio = ">=0.21.1,<1.1.0"
49
+ pytest-asyncio = ">=0.21.1,<1.2.0"
50
50
  syrupy = "^4.5.0"
51
51
  pytest-timeout = "^2.4.0"
52
52
 
@@ -12,6 +12,8 @@ from .exceptions import (
12
12
  EnvoyProbeFailed,
13
13
  )
14
14
  from .models.acb import EnvoyACBPower, EnvoyBatteryAggregate
15
+ from .models.c6combiner import EnvoyC6CC
16
+ from .models.collar import EnvoyCollar
15
17
  from .models.dry_contacts import EnvoyDryContactSettings, EnvoyDryContactStatus
16
18
  from .models.encharge import EnvoyEncharge, EnvoyEnchargeAggregate, EnvoyEnchargePower
17
19
  from .models.enpower import EnvoyEnpower
@@ -45,5 +47,7 @@ __all__ = (
45
47
  "EnvoyBatteryAggregate",
46
48
  "EnvoyDryContactSettings",
47
49
  "EnvoyDryContactStatus",
50
+ "EnvoyCollar",
51
+ "EnvoyC6CC",
48
52
  "EnvoyTariff",
49
53
  )
@@ -101,6 +101,8 @@ class SupportedFeatures(enum.IntFlag):
101
101
  GENERATOR = 2048 #: Envoy reports generator data
102
102
  ACB = 4096 #: Envoy reports ACB Battery data
103
103
  DETAILED_INVERTERS = 8192 #: Detailed inverter data is reported
104
+ COLLAR = 0x4000 #: Envoy reports a Collar
105
+ C6CC = 0x8000 #: Envoy reports a C6 Combiner controller
104
106
 
105
107
 
106
108
  class PhaseNames(enum.StrEnum):
@@ -0,0 +1,59 @@
1
+ """Model for the Enphase C6 Combiner."""
2
+
3
+ # Data Source: URL_ENSEMBLE_INVENTORY
4
+
5
+ from __future__ import annotations
6
+
7
+ from dataclasses import dataclass
8
+ from typing import Any
9
+
10
+ # Required keys for C6 Combiner Controller inventories
11
+ C6CC_REQUIRED_KEYS: frozenset[str] = frozenset(
12
+ {
13
+ "admin_state",
14
+ "admin_state_str",
15
+ "communicating",
16
+ "img_load_date",
17
+ "installed",
18
+ "last_rpt_date",
19
+ "part_num",
20
+ "serial_num",
21
+ "dmir_version",
22
+ "fw_version",
23
+ }
24
+ )
25
+
26
+
27
+ @dataclass(slots=True)
28
+ class EnvoyC6CC:
29
+ """Model for the Enphase C6 Combiner."""
30
+
31
+ admin_state: int
32
+ admin_state_str: str
33
+ firmware_loaded_date: int
34
+ firmware_version: str
35
+ installed_date: int
36
+ last_report_date: int
37
+ communicating: bool
38
+ part_number: str
39
+ serial_number: str
40
+ dmir_version: str
41
+
42
+ @classmethod
43
+ def from_api(cls, inventory: dict[str, Any]) -> EnvoyC6CC | None:
44
+ """Initialize from the API. Returns None if required keys are missing."""
45
+ missing_keys = C6CC_REQUIRED_KEYS - set(inventory)
46
+ if missing_keys:
47
+ return None
48
+ return cls(
49
+ admin_state=inventory["admin_state"],
50
+ admin_state_str=inventory["admin_state_str"],
51
+ communicating=inventory["communicating"],
52
+ firmware_loaded_date=inventory["img_load_date"],
53
+ firmware_version=inventory["fw_version"],
54
+ installed_date=inventory["installed"],
55
+ last_report_date=inventory["last_rpt_date"],
56
+ part_number=inventory["part_num"],
57
+ serial_number=inventory["serial_num"],
58
+ dmir_version=inventory["dmir_version"],
59
+ )
@@ -0,0 +1,73 @@
1
+ """Model for the IQ Meter Collar."""
2
+
3
+ # Data Source: URL_ENSEMBLE_INVENTORY
4
+
5
+ from __future__ import annotations
6
+
7
+ from dataclasses import dataclass
8
+ from typing import Any
9
+
10
+ # Required keys for IQ Meter Collar inventories
11
+ COLLAR_REQUIRED_KEYS: frozenset[str] = frozenset(
12
+ {
13
+ "admin_state",
14
+ "admin_state_str",
15
+ "communicating",
16
+ "img_load_date",
17
+ "img_pnum_running",
18
+ "installed",
19
+ "last_rpt_date",
20
+ "part_num",
21
+ "serial_num",
22
+ "temperature",
23
+ "mid_state",
24
+ "grid_state",
25
+ "control_error",
26
+ "collar_state",
27
+ }
28
+ )
29
+
30
+
31
+ @dataclass(slots=True)
32
+ class EnvoyCollar:
33
+ """Model for the Enphase IQ Meter Collar."""
34
+
35
+ admin_state: int
36
+ admin_state_str: str
37
+ firmware_loaded_date: int
38
+ firmware_version: str
39
+ installed_date: int
40
+ last_report_date: int
41
+ communicating: bool
42
+ mid_state: str
43
+ grid_state: str
44
+ part_number: str
45
+ serial_number: str
46
+ temperature: int
47
+ temperature_unit: str
48
+ control_error: int
49
+ collar_state: str
50
+
51
+ @classmethod
52
+ def from_api(cls, inventory: dict[str, Any]) -> EnvoyCollar | None:
53
+ """Initialize from the API. Returns None if required keys are missing."""
54
+ missing_keys = COLLAR_REQUIRED_KEYS - set(inventory)
55
+ if missing_keys:
56
+ return None
57
+ return cls(
58
+ admin_state=inventory["admin_state"],
59
+ admin_state_str=inventory["admin_state_str"],
60
+ communicating=inventory["communicating"],
61
+ firmware_loaded_date=inventory["img_load_date"],
62
+ firmware_version=inventory["img_pnum_running"],
63
+ installed_date=inventory["installed"],
64
+ last_report_date=inventory["last_rpt_date"],
65
+ part_number=inventory["part_num"],
66
+ serial_number=inventory["serial_num"],
67
+ temperature=inventory["temperature"],
68
+ temperature_unit="C",
69
+ mid_state=inventory["mid_state"],
70
+ grid_state=inventory["grid_state"],
71
+ control_error=inventory["control_error"],
72
+ collar_state=inventory["collar_state"],
73
+ )
@@ -25,6 +25,7 @@ class DryContactType(StrEnum):
25
25
  NONE = "NONE"
26
26
  PV = "PV"
27
27
  LOAD = "LOAD"
28
+ THIRD_PARTY_PV = "3RD-PV"
28
29
 
29
30
 
30
31
  class DryContactMode(StrEnum):
@@ -4,6 +4,8 @@ from dataclasses import dataclass, field
4
4
  from typing import Any
5
5
 
6
6
  from .acb import EnvoyACBPower, EnvoyBatteryAggregate
7
+ from .c6combiner import EnvoyC6CC
8
+ from .collar import EnvoyCollar
7
9
  from .dry_contacts import EnvoyDryContactSettings, EnvoyDryContactStatus
8
10
  from .encharge import EnvoyEncharge, EnvoyEnchargeAggregate, EnvoyEnchargePower
9
11
  from .enpower import EnvoyEnpower
@@ -36,6 +38,10 @@ class EnvoyData:
36
38
  acb_power: EnvoyACBPower | None = None
37
39
  #: aggregated Enphase and ACB battery SOC and total capacity
38
40
  battery_aggregate: EnvoyBatteryAggregate | None = None
41
+ #: IQ Meter collar, only for Envoy with IQ Meter Collar integrated consumption metering installed
42
+ collar: EnvoyCollar | None = None
43
+ #: Envoy C6 Combiner controller
44
+ c6cc: EnvoyC6CC | None = None
39
45
  #: Consumption power & energy values, only for Envoy metered with CT installed
40
46
  system_consumption: EnvoySystemConsumption | None = None
41
47
  #: Solar Production power & energy values
@@ -14,6 +14,8 @@ from ..const import (
14
14
  )
15
15
  from ..exceptions import ENDPOINT_PROBE_EXCEPTIONS
16
16
  from ..models.acb import EnvoyBatteryAggregate
17
+ from ..models.c6combiner import EnvoyC6CC
18
+ from ..models.collar import EnvoyCollar
17
19
  from ..models.dry_contacts import EnvoyDryContactSettings, EnvoyDryContactStatus
18
20
  from ..models.encharge import EnvoyEncharge, EnvoyEnchargeAggregate, EnvoyEnchargePower
19
21
  from ..models.enpower import EnvoyEnpower
@@ -51,6 +53,10 @@ class EnvoyEnembleUpdater(EnvoyUpdater):
51
53
  self._supported_features |= SupportedFeatures.ENPOWER
52
54
  if item["type"] == "ENCHARGE":
53
55
  self._supported_features |= SupportedFeatures.ENCHARGE
56
+ if item["type"] == "COLLAR":
57
+ self._supported_features |= SupportedFeatures.COLLAR
58
+ if item["type"] == "C6 COMBINER CONTROLLER":
59
+ self._supported_features |= SupportedFeatures.C6CC
54
60
 
55
61
  return self._supported_features
56
62
 
@@ -132,3 +138,23 @@ class EnvoyEnembleUpdater(EnvoyUpdater):
132
138
  envoy_data.battery_aggregate = EnvoyBatteryAggregate.from_api(
133
139
  ensemble_secctrl_data
134
140
  )
141
+
142
+ # IQ Meter collar seems like a single instance only
143
+ if supported_features & SupportedFeatures.COLLAR:
144
+ # Update Collar data
145
+ for item in ensemble_inventory_data:
146
+ if item["type"] != "COLLAR":
147
+ continue
148
+ if item.get("devices"):
149
+ collar_data = item["devices"][0]
150
+ envoy_data.collar = EnvoyCollar.from_api(collar_data)
151
+
152
+ # C6 Combiner seems like a single instance only
153
+ if supported_features & SupportedFeatures.C6CC:
154
+ # Update C6CC data
155
+ for item in ensemble_inventory_data:
156
+ if item["type"] != "C6 COMBINER CONTROLLER":
157
+ continue
158
+ if item.get("devices"):
159
+ c6cc_data = item["devices"][0]
160
+ envoy_data.c6cc = EnvoyC6CC.from_api(c6cc_data)
File without changes
File without changes