Homevolt 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- homevolt/__init__.py +24 -0
- homevolt/const.py +23 -0
- homevolt/device.py +329 -0
- homevolt/exceptions.py +26 -0
- homevolt/homevolt.py +82 -0
- homevolt/models.py +43 -0
- homevolt-0.1.0.dist-info/METADATA +174 -0
- homevolt-0.1.0.dist-info/RECORD +10 -0
- homevolt-0.1.0.dist-info/WHEEL +5 -0
- homevolt-0.1.0.dist-info/top_level.txt +1 -0
homevolt/__init__.py
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""Python library for Homevolt EMS devices."""
|
|
2
|
+
|
|
3
|
+
from .device import Device
|
|
4
|
+
from .exceptions import (
|
|
5
|
+
HomevoltAuthenticationError,
|
|
6
|
+
HomevoltConnectionError,
|
|
7
|
+
HomevoltDataError,
|
|
8
|
+
HomevoltException,
|
|
9
|
+
)
|
|
10
|
+
from .homevolt import Homevolt
|
|
11
|
+
from .models import DeviceMetadata, Sensor, SensorType
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"Device",
|
|
15
|
+
"DeviceMetadata",
|
|
16
|
+
"Homevolt",
|
|
17
|
+
"HomevoltAuthenticationError",
|
|
18
|
+
"HomevoltConnectionError",
|
|
19
|
+
"HomevoltDataError",
|
|
20
|
+
"HomevoltException",
|
|
21
|
+
"Sensor",
|
|
22
|
+
"SensorType",
|
|
23
|
+
]
|
|
24
|
+
|
homevolt/const.py
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Constants for the Homevolt library."""
|
|
2
|
+
|
|
3
|
+
# API endpoints
|
|
4
|
+
ENDPOINT_EMS = "/ems.json"
|
|
5
|
+
ENDPOINT_SCHEDULE = "/schedule.json"
|
|
6
|
+
|
|
7
|
+
SCHEDULE_TYPE = {
|
|
8
|
+
0: "Idle",
|
|
9
|
+
1: "Charge Setpoint",
|
|
10
|
+
2: "Discharge Setpoint",
|
|
11
|
+
3: "Charge Grid Setpoint",
|
|
12
|
+
4: "Discharge Grid Setpoint",
|
|
13
|
+
5: "Charge/Discharge Grid Setpoint",
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
# Device type mappings for sensors
|
|
17
|
+
DEVICE_MAP = {
|
|
18
|
+
"grid": "grid",
|
|
19
|
+
"solar": "solar",
|
|
20
|
+
"load": "load",
|
|
21
|
+
"house": "load",
|
|
22
|
+
}
|
|
23
|
+
|
homevolt/device.py
ADDED
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
"""Device class for Homevolt EMS devices."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
import aiohttp
|
|
9
|
+
|
|
10
|
+
from .const import DEVICE_MAP, ENDPOINT_EMS, ENDPOINT_SCHEDULE, SCHEDULE_TYPE
|
|
11
|
+
from .exceptions import (
|
|
12
|
+
HomevoltAuthenticationError,
|
|
13
|
+
HomevoltConnectionError,
|
|
14
|
+
HomevoltDataError,
|
|
15
|
+
)
|
|
16
|
+
from .models import DeviceMetadata, Sensor, SensorType
|
|
17
|
+
|
|
18
|
+
_LOGGER = logging.getLogger(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class Device:
|
|
22
|
+
"""Represents a Homevolt EMS device."""
|
|
23
|
+
|
|
24
|
+
def __init__(
|
|
25
|
+
self,
|
|
26
|
+
ip_address: str,
|
|
27
|
+
password: str | None,
|
|
28
|
+
websession: aiohttp.ClientSession,
|
|
29
|
+
) -> None:
|
|
30
|
+
"""Initialize the device.
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
ip_address: IP address of the Homevolt device
|
|
34
|
+
password: Optional password for authentication
|
|
35
|
+
websession: aiohttp ClientSession for making requests
|
|
36
|
+
"""
|
|
37
|
+
self._ip_address = ip_address
|
|
38
|
+
self._password = password
|
|
39
|
+
self._websession = websession
|
|
40
|
+
self._auth = aiohttp.BasicAuth("admin", password) if password else None
|
|
41
|
+
|
|
42
|
+
self.device_id: str | None = None
|
|
43
|
+
self.sensors: dict[str, Sensor] = {}
|
|
44
|
+
self.device_metadata: dict[str, DeviceMetadata] = {}
|
|
45
|
+
self.current_schedule: dict[str, Any] | None = None
|
|
46
|
+
|
|
47
|
+
async def update_info(self) -> None:
|
|
48
|
+
"""Fetch and update all device information."""
|
|
49
|
+
await self.fetch_ems_data()
|
|
50
|
+
await self.fetch_schedule_data()
|
|
51
|
+
|
|
52
|
+
async def fetch_ems_data(self) -> None:
|
|
53
|
+
"""Fetch EMS data from the device."""
|
|
54
|
+
try:
|
|
55
|
+
url = f"http://{self._ip_address}{ENDPOINT_EMS}"
|
|
56
|
+
async with self._websession.get(url, auth=self._auth) as response:
|
|
57
|
+
if response.status == 401:
|
|
58
|
+
raise HomevoltAuthenticationError("Authentication failed")
|
|
59
|
+
response.raise_for_status()
|
|
60
|
+
ems_data = await response.json()
|
|
61
|
+
except aiohttp.ClientError as err:
|
|
62
|
+
raise HomevoltConnectionError(f"Failed to connect to device: {err}") from err
|
|
63
|
+
except Exception as err:
|
|
64
|
+
raise HomevoltDataError(f"Failed to parse EMS data: {err}") from err
|
|
65
|
+
|
|
66
|
+
self._parse_ems_data(ems_data)
|
|
67
|
+
|
|
68
|
+
async def fetch_schedule_data(self) -> None:
|
|
69
|
+
"""Fetch schedule data from the device."""
|
|
70
|
+
try:
|
|
71
|
+
url = f"http://{self._ip_address}{ENDPOINT_SCHEDULE}"
|
|
72
|
+
async with self._websession.get(url, auth=self._auth) as response:
|
|
73
|
+
if response.status == 401:
|
|
74
|
+
raise HomevoltAuthenticationError("Authentication failed")
|
|
75
|
+
response.raise_for_status()
|
|
76
|
+
schedule_data = await response.json()
|
|
77
|
+
except aiohttp.ClientError as err:
|
|
78
|
+
raise HomevoltConnectionError(f"Failed to connect to device: {err}") from err
|
|
79
|
+
except Exception as err:
|
|
80
|
+
raise HomevoltDataError(f"Failed to parse schedule data: {err}") from err
|
|
81
|
+
|
|
82
|
+
self._parse_schedule_data(schedule_data)
|
|
83
|
+
|
|
84
|
+
def _parse_ems_data(self, ems_data: dict[str, Any]) -> None:
|
|
85
|
+
"""Parse EMS JSON response."""
|
|
86
|
+
if not ems_data.get("ems") or not ems_data["ems"]:
|
|
87
|
+
raise HomevoltDataError("No EMS data found in response")
|
|
88
|
+
|
|
89
|
+
device_id = str(ems_data["ems"][0]["ecu_id"])
|
|
90
|
+
self.device_id = device_id
|
|
91
|
+
ems_device_id = f"ems_{device_id}"
|
|
92
|
+
|
|
93
|
+
# Initialize device metadata
|
|
94
|
+
self.device_metadata = {
|
|
95
|
+
ems_device_id: DeviceMetadata(name=f"Homevolt EMS {device_id}", model="Homevolt EMS"),
|
|
96
|
+
"grid": DeviceMetadata(name="Homevolt Grid Sensor", model="Grid Sensor"),
|
|
97
|
+
"solar": DeviceMetadata(name="Homevolt Solar Sensor", model="Solar Sensor"),
|
|
98
|
+
"load": DeviceMetadata(name="Homevolt Load Sensor", model="Load Sensor"),
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
# Initialize sensors dictionary
|
|
102
|
+
self.sensors = {}
|
|
103
|
+
|
|
104
|
+
# EMS device sensors - all main EMS data
|
|
105
|
+
ems = ems_data["ems"][0]
|
|
106
|
+
self.sensors.update(
|
|
107
|
+
{
|
|
108
|
+
"L1 Voltage": Sensor(
|
|
109
|
+
value=ems["ems_voltage"]["l1"] / 10,
|
|
110
|
+
type=SensorType.VOLTAGE,
|
|
111
|
+
device_identifier=ems_device_id,
|
|
112
|
+
),
|
|
113
|
+
"L2 Voltage": Sensor(
|
|
114
|
+
value=ems["ems_voltage"]["l2"] / 10,
|
|
115
|
+
type=SensorType.VOLTAGE,
|
|
116
|
+
device_identifier=ems_device_id,
|
|
117
|
+
),
|
|
118
|
+
"L3 Voltage": Sensor(
|
|
119
|
+
value=ems["ems_voltage"]["l3"] / 10,
|
|
120
|
+
type=SensorType.VOLTAGE,
|
|
121
|
+
device_identifier=ems_device_id,
|
|
122
|
+
),
|
|
123
|
+
"L1_L2 Voltage": Sensor(
|
|
124
|
+
value=ems["ems_voltage"]["l1_l2"] / 10,
|
|
125
|
+
type=SensorType.VOLTAGE,
|
|
126
|
+
device_identifier=ems_device_id,
|
|
127
|
+
),
|
|
128
|
+
"L2_L3 Voltage": Sensor(
|
|
129
|
+
value=ems["ems_voltage"]["l2_l3"] / 10,
|
|
130
|
+
type=SensorType.VOLTAGE,
|
|
131
|
+
device_identifier=ems_device_id,
|
|
132
|
+
),
|
|
133
|
+
"L3_L1 Voltage": Sensor(
|
|
134
|
+
value=ems["ems_voltage"]["l3_l1"] / 10,
|
|
135
|
+
type=SensorType.VOLTAGE,
|
|
136
|
+
device_identifier=ems_device_id,
|
|
137
|
+
),
|
|
138
|
+
"L1 Current": Sensor(
|
|
139
|
+
value=ems["ems_current"]["l1"],
|
|
140
|
+
type=SensorType.CURRENT,
|
|
141
|
+
device_identifier=ems_device_id,
|
|
142
|
+
),
|
|
143
|
+
"L2 Current": Sensor(
|
|
144
|
+
value=ems["ems_current"]["l2"],
|
|
145
|
+
type=SensorType.CURRENT,
|
|
146
|
+
device_identifier=ems_device_id,
|
|
147
|
+
),
|
|
148
|
+
"L3 Current": Sensor(
|
|
149
|
+
value=ems["ems_current"]["l3"],
|
|
150
|
+
type=SensorType.CURRENT,
|
|
151
|
+
device_identifier=ems_device_id,
|
|
152
|
+
),
|
|
153
|
+
"System Temperature": Sensor(
|
|
154
|
+
value=ems["ems_data"]["sys_temp"] / 10.0,
|
|
155
|
+
type=SensorType.TEMPERATURE,
|
|
156
|
+
device_identifier=ems_device_id,
|
|
157
|
+
),
|
|
158
|
+
"Imported Energy": Sensor(
|
|
159
|
+
value=ems["ems_aggregate"]["imported_kwh"],
|
|
160
|
+
type=SensorType.ENERGY_INCREASING,
|
|
161
|
+
device_identifier=ems_device_id,
|
|
162
|
+
),
|
|
163
|
+
"Exported Energy": Sensor(
|
|
164
|
+
value=ems["ems_aggregate"]["exported_kwh"],
|
|
165
|
+
type=SensorType.ENERGY_INCREASING,
|
|
166
|
+
device_identifier=ems_device_id,
|
|
167
|
+
),
|
|
168
|
+
"Available Charging Power": Sensor(
|
|
169
|
+
value=ems["ems_prediction"]["avail_ch_pwr"],
|
|
170
|
+
type=SensorType.POWER,
|
|
171
|
+
device_identifier=ems_device_id,
|
|
172
|
+
),
|
|
173
|
+
"Available Discharge Power": Sensor(
|
|
174
|
+
value=ems["ems_prediction"]["avail_di_pwr"],
|
|
175
|
+
type=SensorType.POWER,
|
|
176
|
+
device_identifier=ems_device_id,
|
|
177
|
+
),
|
|
178
|
+
"Available Charging Energy": Sensor(
|
|
179
|
+
value=ems["ems_prediction"]["avail_ch_energy"],
|
|
180
|
+
type=SensorType.ENERGY_TOTAL,
|
|
181
|
+
device_identifier=ems_device_id,
|
|
182
|
+
),
|
|
183
|
+
"Available Discharge Energy": Sensor(
|
|
184
|
+
value=ems["ems_prediction"]["avail_di_energy"],
|
|
185
|
+
type=SensorType.ENERGY_TOTAL,
|
|
186
|
+
device_identifier=ems_device_id,
|
|
187
|
+
),
|
|
188
|
+
"Power": Sensor(
|
|
189
|
+
value=ems["ems_data"]["power"],
|
|
190
|
+
type=SensorType.POWER,
|
|
191
|
+
device_identifier=ems_device_id,
|
|
192
|
+
),
|
|
193
|
+
"Frequency": Sensor(
|
|
194
|
+
value=ems["ems_data"]["frequency"],
|
|
195
|
+
type=SensorType.FREQUENCY,
|
|
196
|
+
device_identifier=ems_device_id,
|
|
197
|
+
),
|
|
198
|
+
"Battery State of Charge": Sensor(
|
|
199
|
+
value=ems["ems_data"]["soc_avg"] / 100,
|
|
200
|
+
type=SensorType.PERCENTAGE,
|
|
201
|
+
device_identifier=ems_device_id,
|
|
202
|
+
),
|
|
203
|
+
}
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
# Battery sensors
|
|
207
|
+
for bat_id, battery in enumerate(ems.get("bms_data", [])):
|
|
208
|
+
battery_device_id = f"battery_{bat_id}"
|
|
209
|
+
self.device_metadata[battery_device_id] = DeviceMetadata(
|
|
210
|
+
name=f"Homevolt Battery {bat_id}",
|
|
211
|
+
model="Homevolt Battery",
|
|
212
|
+
)
|
|
213
|
+
self.sensors[f"Homevolt battery {bat_id}"] = Sensor(
|
|
214
|
+
value=battery["soc"] / 100,
|
|
215
|
+
type=SensorType.PERCENTAGE,
|
|
216
|
+
device_identifier=battery_device_id,
|
|
217
|
+
)
|
|
218
|
+
self.sensors[f"Homevolt battery {bat_id} tmin"] = Sensor(
|
|
219
|
+
value=battery["tmin"] / 10,
|
|
220
|
+
type=SensorType.TEMPERATURE,
|
|
221
|
+
device_identifier=battery_device_id,
|
|
222
|
+
)
|
|
223
|
+
self.sensors[f"Homevolt battery {bat_id} tmax"] = Sensor(
|
|
224
|
+
value=battery["tmax"] / 10,
|
|
225
|
+
type=SensorType.TEMPERATURE,
|
|
226
|
+
device_identifier=battery_device_id,
|
|
227
|
+
)
|
|
228
|
+
self.sensors[f"Homevolt battery {bat_id} charge cycles"] = Sensor(
|
|
229
|
+
value=battery["cycle_count"],
|
|
230
|
+
type=SensorType.COUNT,
|
|
231
|
+
device_identifier=battery_device_id,
|
|
232
|
+
)
|
|
233
|
+
|
|
234
|
+
# External sensors (grid, solar, load)
|
|
235
|
+
for sensor in ems_data.get("sensors", []):
|
|
236
|
+
if not sensor.get("available"):
|
|
237
|
+
continue
|
|
238
|
+
|
|
239
|
+
sensor_type = sensor["type"]
|
|
240
|
+
sensor_device_id = DEVICE_MAP.get(sensor_type)
|
|
241
|
+
|
|
242
|
+
if not sensor_device_id:
|
|
243
|
+
continue
|
|
244
|
+
|
|
245
|
+
# Calculate total power from all phases
|
|
246
|
+
total_power = sum(phase["power"] for phase in sensor.get("phase", []))
|
|
247
|
+
|
|
248
|
+
self.sensors[f"Power {sensor_type}"] = Sensor(
|
|
249
|
+
value=total_power,
|
|
250
|
+
type=SensorType.POWER,
|
|
251
|
+
device_identifier=sensor_device_id,
|
|
252
|
+
)
|
|
253
|
+
self.sensors[f"Energy imported {sensor_type}"] = Sensor(
|
|
254
|
+
value=sensor.get("energy_imported", 0),
|
|
255
|
+
type=SensorType.ENERGY_INCREASING,
|
|
256
|
+
device_identifier=sensor_device_id,
|
|
257
|
+
)
|
|
258
|
+
self.sensors[f"Energy exported {sensor_type}"] = Sensor(
|
|
259
|
+
value=sensor.get("energy_exported", 0),
|
|
260
|
+
type=SensorType.ENERGY_INCREASING,
|
|
261
|
+
device_identifier=sensor_device_id,
|
|
262
|
+
)
|
|
263
|
+
self.sensors[f"RSSI {sensor_type}"] = Sensor(
|
|
264
|
+
value=sensor.get("rssi"),
|
|
265
|
+
type=SensorType.SIGNAL_STRENGTH,
|
|
266
|
+
device_identifier=sensor_device_id,
|
|
267
|
+
)
|
|
268
|
+
self.sensors[f"Average RSSI {sensor_type}"] = Sensor(
|
|
269
|
+
value=sensor.get("average_rssi"),
|
|
270
|
+
type=SensorType.SIGNAL_STRENGTH,
|
|
271
|
+
device_identifier=sensor_device_id,
|
|
272
|
+
)
|
|
273
|
+
|
|
274
|
+
# Phase-specific sensors
|
|
275
|
+
for phase_name, phase in zip(["L1", "L2", "L3"], sensor.get("phase", []), strict=False):
|
|
276
|
+
self.sensors[f"{phase_name} Voltage {sensor_type}"] = Sensor(
|
|
277
|
+
value=phase.get("voltage"),
|
|
278
|
+
type=SensorType.VOLTAGE,
|
|
279
|
+
device_identifier=sensor_device_id,
|
|
280
|
+
)
|
|
281
|
+
self.sensors[f"{phase_name} Current {sensor_type}"] = Sensor(
|
|
282
|
+
value=phase.get("amp"),
|
|
283
|
+
type=SensorType.CURRENT,
|
|
284
|
+
device_identifier=sensor_device_id,
|
|
285
|
+
)
|
|
286
|
+
self.sensors[f"{phase_name} Power {sensor_type}"] = Sensor(
|
|
287
|
+
value=phase.get("power"),
|
|
288
|
+
type=SensorType.POWER,
|
|
289
|
+
device_identifier=sensor_device_id,
|
|
290
|
+
)
|
|
291
|
+
|
|
292
|
+
def _parse_schedule_data(self, schedule_data: dict[str, Any]) -> None:
|
|
293
|
+
"""Parse schedule JSON response."""
|
|
294
|
+
self.current_schedule = schedule_data
|
|
295
|
+
|
|
296
|
+
if not self.device_id:
|
|
297
|
+
return
|
|
298
|
+
|
|
299
|
+
ems_device_id = f"ems_{self.device_id}"
|
|
300
|
+
|
|
301
|
+
self.sensors["Schedule id"] = Sensor(
|
|
302
|
+
value=schedule_data.get("schedule_id"),
|
|
303
|
+
type=SensorType.TEXT,
|
|
304
|
+
device_identifier=ems_device_id,
|
|
305
|
+
)
|
|
306
|
+
|
|
307
|
+
schedule = schedule_data.get("schedule", [{}])[0] if schedule_data.get("schedule") else {"type": -1, "params": {}}
|
|
308
|
+
|
|
309
|
+
self.sensors["Schedule Type"] = Sensor(
|
|
310
|
+
value=SCHEDULE_TYPE.get(schedule.get("type", -1)),
|
|
311
|
+
type=SensorType.SCHEDULE_TYPE,
|
|
312
|
+
device_identifier=ems_device_id,
|
|
313
|
+
)
|
|
314
|
+
self.sensors["Schedule Power Setpoint"] = Sensor(
|
|
315
|
+
value=schedule.get("params", {}).get("setpoint"),
|
|
316
|
+
type=SensorType.POWER,
|
|
317
|
+
device_identifier=ems_device_id,
|
|
318
|
+
)
|
|
319
|
+
self.sensors["Schedule Max Power"] = Sensor(
|
|
320
|
+
value=schedule.get("max_charge"),
|
|
321
|
+
type=SensorType.POWER,
|
|
322
|
+
device_identifier=ems_device_id,
|
|
323
|
+
)
|
|
324
|
+
self.sensors["Schedule Max Discharge"] = Sensor(
|
|
325
|
+
value=schedule.get("max_discharge"),
|
|
326
|
+
type=SensorType.POWER,
|
|
327
|
+
device_identifier=ems_device_id,
|
|
328
|
+
)
|
|
329
|
+
|
homevolt/exceptions.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
"""Custom exceptions for the Homevolt library."""
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class HomevoltException(Exception):
|
|
5
|
+
"""Base exception for all Homevolt errors."""
|
|
6
|
+
|
|
7
|
+
pass
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class HomevoltConnectionError(HomevoltException):
|
|
11
|
+
"""Raised when there's a connection or network error."""
|
|
12
|
+
|
|
13
|
+
pass
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class HomevoltAuthenticationError(HomevoltException):
|
|
17
|
+
"""Raised when authentication fails."""
|
|
18
|
+
|
|
19
|
+
pass
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class HomevoltDataError(HomevoltException):
|
|
23
|
+
"""Raised when there's an error parsing or processing data."""
|
|
24
|
+
|
|
25
|
+
pass
|
|
26
|
+
|
homevolt/homevolt.py
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
"""Main Homevolt class for connecting to EMS devices."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import logging
|
|
6
|
+
|
|
7
|
+
import aiohttp
|
|
8
|
+
|
|
9
|
+
from .device import Device
|
|
10
|
+
|
|
11
|
+
_LOGGER = logging.getLogger(__name__)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Homevolt:
|
|
15
|
+
"""Main class for interacting with Homevolt EMS devices."""
|
|
16
|
+
|
|
17
|
+
def __init__(
|
|
18
|
+
self,
|
|
19
|
+
ip_address: str,
|
|
20
|
+
password: str | None = None,
|
|
21
|
+
websession: aiohttp.ClientSession | None = None,
|
|
22
|
+
) -> None:
|
|
23
|
+
"""Initialize the Homevolt connection.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
ip_address: IP address of the Homevolt device
|
|
27
|
+
password: Optional password for authentication
|
|
28
|
+
websession: Optional aiohttp ClientSession. If not provided, one will be created.
|
|
29
|
+
"""
|
|
30
|
+
self._ip_address = ip_address
|
|
31
|
+
self._password = password
|
|
32
|
+
self._websession = websession
|
|
33
|
+
self._own_session = websession is None
|
|
34
|
+
|
|
35
|
+
self._device: Device | None = None
|
|
36
|
+
|
|
37
|
+
async def update_info(self) -> None:
|
|
38
|
+
"""Fetch and update device information."""
|
|
39
|
+
if self._device is None:
|
|
40
|
+
await self._ensure_session()
|
|
41
|
+
self._device = Device(
|
|
42
|
+
ip_address=self._ip_address,
|
|
43
|
+
password=self._password,
|
|
44
|
+
websession=self._websession,
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
await self._device.update_info()
|
|
48
|
+
|
|
49
|
+
def get_device(self) -> Device:
|
|
50
|
+
"""Get the device object.
|
|
51
|
+
|
|
52
|
+
Returns:
|
|
53
|
+
The Device object for this Homevolt connection
|
|
54
|
+
|
|
55
|
+
Raises:
|
|
56
|
+
RuntimeError: If device information hasn't been fetched yet
|
|
57
|
+
"""
|
|
58
|
+
if self._device is None:
|
|
59
|
+
raise RuntimeError("Device information not yet fetched. Call update_info() first.")
|
|
60
|
+
return self._device
|
|
61
|
+
|
|
62
|
+
async def close_connection(self) -> None:
|
|
63
|
+
"""Close the connection and clean up resources."""
|
|
64
|
+
if self._own_session and self._websession:
|
|
65
|
+
await self._websession.close()
|
|
66
|
+
self._websession = None
|
|
67
|
+
|
|
68
|
+
async def _ensure_session(self) -> None:
|
|
69
|
+
"""Ensure a websession exists."""
|
|
70
|
+
if self._websession is None:
|
|
71
|
+
self._websession = aiohttp.ClientSession()
|
|
72
|
+
self._own_session = True
|
|
73
|
+
|
|
74
|
+
async def __aenter__(self) -> Homevolt:
|
|
75
|
+
"""Async context manager entry."""
|
|
76
|
+
await self._ensure_session()
|
|
77
|
+
return self
|
|
78
|
+
|
|
79
|
+
async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
|
|
80
|
+
"""Async context manager exit."""
|
|
81
|
+
await self.close_connection()
|
|
82
|
+
|
homevolt/models.py
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
"""Data models for Homevolt library."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
from enum import Enum
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass
|
|
10
|
+
class DeviceMetadata:
|
|
11
|
+
"""Metadata for device information."""
|
|
12
|
+
|
|
13
|
+
name: str
|
|
14
|
+
model: str
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class SensorType(Enum):
|
|
18
|
+
"""Enumeration of sensor types."""
|
|
19
|
+
|
|
20
|
+
COUNT = "count"
|
|
21
|
+
CURRENT = "current"
|
|
22
|
+
ENERGY_INCREASING = "energy_increasing"
|
|
23
|
+
ENERGY_TOTAL = "energy_total"
|
|
24
|
+
ENERGY = "energy"
|
|
25
|
+
FREQUENCY = "frequency"
|
|
26
|
+
POWER = "power"
|
|
27
|
+
VOLTAGE = "voltage"
|
|
28
|
+
SIGNAL_STRENGTH = "signal_strength"
|
|
29
|
+
PERCENTAGE = "percentage"
|
|
30
|
+
SCHEDULE_TYPE = "schedule_type"
|
|
31
|
+
SCHEDULE_PARAMS = "schedule_params"
|
|
32
|
+
TEMPERATURE = "temperature"
|
|
33
|
+
TEXT = "text"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
@dataclass
|
|
37
|
+
class Sensor:
|
|
38
|
+
"""Represents a sensor reading."""
|
|
39
|
+
|
|
40
|
+
value: float | str | None
|
|
41
|
+
type: SensorType
|
|
42
|
+
device_identifier: str = "main" # Device identifier for grouping sensors into devices
|
|
43
|
+
|
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: Homevolt
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Python library for Homevolt EMS devices
|
|
5
|
+
Author-email: Your Name <your.email@example.com>
|
|
6
|
+
License: GPL-3.0
|
|
7
|
+
Classifier: Development Status :: 4 - Beta
|
|
8
|
+
Classifier: Intended Audience :: Developers
|
|
9
|
+
Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
13
|
+
Requires-Python: >=3.12
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
Requires-Dist: aiohttp>=3.8.0
|
|
16
|
+
|
|
17
|
+
# pyHomevolt
|
|
18
|
+
|
|
19
|
+
Python library for Homevolt EMS devices.
|
|
20
|
+
|
|
21
|
+
Get real-time data from your Homevolt Energy Management System, including:
|
|
22
|
+
- Voltage, current, and power measurements
|
|
23
|
+
- Battery state of charge and temperature
|
|
24
|
+
- Grid, solar, and load sensor data
|
|
25
|
+
- Schedule information
|
|
26
|
+
|
|
27
|
+
## Install
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
pip install pyHomevolt
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Example
|
|
34
|
+
|
|
35
|
+
```python
|
|
36
|
+
import asyncio
|
|
37
|
+
import aiohttp
|
|
38
|
+
import homevolt
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
async def main():
|
|
42
|
+
async with aiohttp.ClientSession() as session:
|
|
43
|
+
homevolt_connection = homevolt.Homevolt(
|
|
44
|
+
ip_address="192.168.1.100",
|
|
45
|
+
password="optional_password",
|
|
46
|
+
websession=session,
|
|
47
|
+
)
|
|
48
|
+
await homevolt_connection.update_info()
|
|
49
|
+
|
|
50
|
+
device = homevolt_connection.get_device()
|
|
51
|
+
print(f"Device ID: {device.device_id}")
|
|
52
|
+
print(f"Current Power: {device.sensors['Power'].value} W")
|
|
53
|
+
print(f"Battery SOC: {device.sensors['Battery State of Charge'].value * 100}%")
|
|
54
|
+
|
|
55
|
+
# Access all sensors
|
|
56
|
+
for sensor_name, sensor in device.sensors.items():
|
|
57
|
+
print(f"{sensor_name}: {sensor.value} ({sensor.type.value})")
|
|
58
|
+
|
|
59
|
+
# Access device metadata
|
|
60
|
+
for device_id, metadata in device.device_metadata.items():
|
|
61
|
+
print(f"{device_id}: {metadata.name} ({metadata.model})")
|
|
62
|
+
|
|
63
|
+
await homevolt_connection.close_connection()
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
if __name__ == "__main__":
|
|
67
|
+
asyncio.run(main())
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Example with context manager
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
import asyncio
|
|
74
|
+
import aiohttp
|
|
75
|
+
import homevolt
|
|
76
|
+
|
|
77
|
+
|
|
78
|
+
async def main():
|
|
79
|
+
async with aiohttp.ClientSession() as session:
|
|
80
|
+
async with homevolt.Homevolt(
|
|
81
|
+
ip_address="192.168.1.100",
|
|
82
|
+
password="optional_password",
|
|
83
|
+
websession=session,
|
|
84
|
+
) as homevolt_connection:
|
|
85
|
+
await homevolt_connection.update_info()
|
|
86
|
+
|
|
87
|
+
device = homevolt_connection.get_device()
|
|
88
|
+
await device.update_info() # Refresh data
|
|
89
|
+
|
|
90
|
+
print(f"Device ID: {device.device_id}")
|
|
91
|
+
print(f"Available sensors: {list(device.sensors.keys())}")
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
if __name__ == "__main__":
|
|
95
|
+
asyncio.run(main())
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## API Reference
|
|
99
|
+
|
|
100
|
+
### Homevolt
|
|
101
|
+
|
|
102
|
+
Main class for connecting to a Homevolt device.
|
|
103
|
+
|
|
104
|
+
#### `Homevolt(ip_address, password=None, websession=None)`
|
|
105
|
+
|
|
106
|
+
Initialize a Homevolt connection.
|
|
107
|
+
|
|
108
|
+
- `ip_address` (str): IP address of the Homevolt device
|
|
109
|
+
- `password` (str, optional): Password for authentication
|
|
110
|
+
- `websession` (aiohttp.ClientSession, optional): HTTP session. If not provided, one will be created.
|
|
111
|
+
|
|
112
|
+
#### Methods
|
|
113
|
+
|
|
114
|
+
- `async update_info()`: Fetch and update device information
|
|
115
|
+
- `get_device()`: Get the Device object
|
|
116
|
+
- `async close_connection()`: Close the connection and clean up resources
|
|
117
|
+
|
|
118
|
+
### Device
|
|
119
|
+
|
|
120
|
+
Represents a Homevolt EMS device.
|
|
121
|
+
|
|
122
|
+
#### Properties
|
|
123
|
+
|
|
124
|
+
- `device_id` (str): Device identifier
|
|
125
|
+
- `sensors` (dict[str, Sensor]): Dictionary of sensor readings
|
|
126
|
+
- `device_metadata` (dict[str, DeviceMetadata]): Dictionary of device metadata
|
|
127
|
+
- `current_schedule` (dict): Current schedule information
|
|
128
|
+
|
|
129
|
+
#### Methods
|
|
130
|
+
|
|
131
|
+
- `async update_info()`: Fetch latest EMS and schedule data
|
|
132
|
+
- `async fetch_ems_data()`: Fetch EMS data specifically
|
|
133
|
+
- `async fetch_schedule_data()`: Fetch schedule data specifically
|
|
134
|
+
|
|
135
|
+
### Data Models
|
|
136
|
+
|
|
137
|
+
#### Sensor
|
|
138
|
+
|
|
139
|
+
- `value` (float | str | None): Sensor value
|
|
140
|
+
- `type` (SensorType): Type of sensor
|
|
141
|
+
- `device_identifier` (str): Device identifier for grouping sensors
|
|
142
|
+
|
|
143
|
+
#### DeviceMetadata
|
|
144
|
+
|
|
145
|
+
- `name` (str): Device name
|
|
146
|
+
- `model` (str): Device model
|
|
147
|
+
|
|
148
|
+
#### SensorType
|
|
149
|
+
|
|
150
|
+
Enumeration of sensor types:
|
|
151
|
+
- `VOLTAGE`
|
|
152
|
+
- `CURRENT`
|
|
153
|
+
- `POWER`
|
|
154
|
+
- `ENERGY_INCREASING`
|
|
155
|
+
- `ENERGY_TOTAL`
|
|
156
|
+
- `FREQUENCY`
|
|
157
|
+
- `TEMPERATURE`
|
|
158
|
+
- `PERCENTAGE`
|
|
159
|
+
- `SIGNAL_STRENGTH`
|
|
160
|
+
- `COUNT`
|
|
161
|
+
- `TEXT`
|
|
162
|
+
- `SCHEDULE_TYPE`
|
|
163
|
+
|
|
164
|
+
### Exceptions
|
|
165
|
+
|
|
166
|
+
- `HomevoltException`: Base exception for all Homevolt errors
|
|
167
|
+
- `HomevoltConnectionError`: Connection or network errors
|
|
168
|
+
- `HomevoltAuthenticationError`: Authentication failures
|
|
169
|
+
- `HomevoltDataError`: Data parsing errors
|
|
170
|
+
|
|
171
|
+
## License
|
|
172
|
+
|
|
173
|
+
GPL-3.0
|
|
174
|
+
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
homevolt/__init__.py,sha256=ClavtnqHSLlElc7yiUCnDFzIwHyTPWBqGmvsg_4iqnw,513
|
|
2
|
+
homevolt/const.py,sha256=0PpvS_X9aE0t2B-ekj_BhA1qNKQHqo4r2EoF3hJKxqM,457
|
|
3
|
+
homevolt/device.py,sha256=xWmfguczg-ZNM1Xj1_YTwvNe2GsKCa8lCKxGmsOTfvA,13271
|
|
4
|
+
homevolt/exceptions.py,sha256=YfXXS944vTKwb6sQ9AWfOsAjY1ONSBYu_DdL_g6NaZc,505
|
|
5
|
+
homevolt/homevolt.py,sha256=XnoWwiY5j8lCNH60S9rBumFQNzn47WIfZVWsFrMEIMo,2468
|
|
6
|
+
homevolt/models.py,sha256=8m3uCGrOiB3FPVhz8k4sUIJTnJENHa96uv4z-0NPRuk,929
|
|
7
|
+
homevolt-0.1.0.dist-info/METADATA,sha256=RDFWrj8iKYkKqBoRX9xj6ECOTeAVi_qh_QRc1nY1Yyc,4533
|
|
8
|
+
homevolt-0.1.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
9
|
+
homevolt-0.1.0.dist-info/top_level.txt,sha256=eOPhiXXDJEiBEdExWtFc_UqWSnbLbFpEsMm3fCXrBTA,9
|
|
10
|
+
homevolt-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
homevolt
|