Homevolt 0.1.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.
@@ -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,13 @@
1
+ README.md
2
+ pyproject.toml
3
+ Homevolt.egg-info/PKG-INFO
4
+ Homevolt.egg-info/SOURCES.txt
5
+ Homevolt.egg-info/dependency_links.txt
6
+ Homevolt.egg-info/requires.txt
7
+ Homevolt.egg-info/top_level.txt
8
+ homevolt/__init__.py
9
+ homevolt/const.py
10
+ homevolt/device.py
11
+ homevolt/exceptions.py
12
+ homevolt/homevolt.py
13
+ homevolt/models.py
@@ -0,0 +1 @@
1
+ aiohttp>=3.8.0
@@ -0,0 +1 @@
1
+ homevolt
@@ -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,158 @@
1
+ # pyHomevolt
2
+
3
+ Python library for Homevolt EMS devices.
4
+
5
+ Get real-time data from your Homevolt Energy Management System, including:
6
+ - Voltage, current, and power measurements
7
+ - Battery state of charge and temperature
8
+ - Grid, solar, and load sensor data
9
+ - Schedule information
10
+
11
+ ## Install
12
+
13
+ ```bash
14
+ pip install pyHomevolt
15
+ ```
16
+
17
+ ## Example
18
+
19
+ ```python
20
+ import asyncio
21
+ import aiohttp
22
+ import homevolt
23
+
24
+
25
+ async def main():
26
+ async with aiohttp.ClientSession() as session:
27
+ homevolt_connection = homevolt.Homevolt(
28
+ ip_address="192.168.1.100",
29
+ password="optional_password",
30
+ websession=session,
31
+ )
32
+ await homevolt_connection.update_info()
33
+
34
+ device = homevolt_connection.get_device()
35
+ print(f"Device ID: {device.device_id}")
36
+ print(f"Current Power: {device.sensors['Power'].value} W")
37
+ print(f"Battery SOC: {device.sensors['Battery State of Charge'].value * 100}%")
38
+
39
+ # Access all sensors
40
+ for sensor_name, sensor in device.sensors.items():
41
+ print(f"{sensor_name}: {sensor.value} ({sensor.type.value})")
42
+
43
+ # Access device metadata
44
+ for device_id, metadata in device.device_metadata.items():
45
+ print(f"{device_id}: {metadata.name} ({metadata.model})")
46
+
47
+ await homevolt_connection.close_connection()
48
+
49
+
50
+ if __name__ == "__main__":
51
+ asyncio.run(main())
52
+ ```
53
+
54
+ ## Example with context manager
55
+
56
+ ```python
57
+ import asyncio
58
+ import aiohttp
59
+ import homevolt
60
+
61
+
62
+ async def main():
63
+ async with aiohttp.ClientSession() as session:
64
+ async with homevolt.Homevolt(
65
+ ip_address="192.168.1.100",
66
+ password="optional_password",
67
+ websession=session,
68
+ ) as homevolt_connection:
69
+ await homevolt_connection.update_info()
70
+
71
+ device = homevolt_connection.get_device()
72
+ await device.update_info() # Refresh data
73
+
74
+ print(f"Device ID: {device.device_id}")
75
+ print(f"Available sensors: {list(device.sensors.keys())}")
76
+
77
+
78
+ if __name__ == "__main__":
79
+ asyncio.run(main())
80
+ ```
81
+
82
+ ## API Reference
83
+
84
+ ### Homevolt
85
+
86
+ Main class for connecting to a Homevolt device.
87
+
88
+ #### `Homevolt(ip_address, password=None, websession=None)`
89
+
90
+ Initialize a Homevolt connection.
91
+
92
+ - `ip_address` (str): IP address of the Homevolt device
93
+ - `password` (str, optional): Password for authentication
94
+ - `websession` (aiohttp.ClientSession, optional): HTTP session. If not provided, one will be created.
95
+
96
+ #### Methods
97
+
98
+ - `async update_info()`: Fetch and update device information
99
+ - `get_device()`: Get the Device object
100
+ - `async close_connection()`: Close the connection and clean up resources
101
+
102
+ ### Device
103
+
104
+ Represents a Homevolt EMS device.
105
+
106
+ #### Properties
107
+
108
+ - `device_id` (str): Device identifier
109
+ - `sensors` (dict[str, Sensor]): Dictionary of sensor readings
110
+ - `device_metadata` (dict[str, DeviceMetadata]): Dictionary of device metadata
111
+ - `current_schedule` (dict): Current schedule information
112
+
113
+ #### Methods
114
+
115
+ - `async update_info()`: Fetch latest EMS and schedule data
116
+ - `async fetch_ems_data()`: Fetch EMS data specifically
117
+ - `async fetch_schedule_data()`: Fetch schedule data specifically
118
+
119
+ ### Data Models
120
+
121
+ #### Sensor
122
+
123
+ - `value` (float | str | None): Sensor value
124
+ - `type` (SensorType): Type of sensor
125
+ - `device_identifier` (str): Device identifier for grouping sensors
126
+
127
+ #### DeviceMetadata
128
+
129
+ - `name` (str): Device name
130
+ - `model` (str): Device model
131
+
132
+ #### SensorType
133
+
134
+ Enumeration of sensor types:
135
+ - `VOLTAGE`
136
+ - `CURRENT`
137
+ - `POWER`
138
+ - `ENERGY_INCREASING`
139
+ - `ENERGY_TOTAL`
140
+ - `FREQUENCY`
141
+ - `TEMPERATURE`
142
+ - `PERCENTAGE`
143
+ - `SIGNAL_STRENGTH`
144
+ - `COUNT`
145
+ - `TEXT`
146
+ - `SCHEDULE_TYPE`
147
+
148
+ ### Exceptions
149
+
150
+ - `HomevoltException`: Base exception for all Homevolt errors
151
+ - `HomevoltConnectionError`: Connection or network errors
152
+ - `HomevoltAuthenticationError`: Authentication failures
153
+ - `HomevoltDataError`: Data parsing errors
154
+
155
+ ## License
156
+
157
+ GPL-3.0
158
+
@@ -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
+
@@ -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
+
@@ -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
+
@@ -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
+
@@ -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
+
@@ -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,41 @@
1
+ [build-system]
2
+ requires = ["setuptools>=61.0", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "Homevolt"
7
+ version = "0.1.0"
8
+ description = "Python library for Homevolt EMS devices"
9
+ readme = "README.md"
10
+ requires-python = ">=3.12"
11
+ license = {text = "GPL-3.0"}
12
+ authors = [
13
+ {name = "Your Name", email = "your.email@example.com"},
14
+ ]
15
+ classifiers = [
16
+ "Development Status :: 4 - Beta",
17
+ "Intended Audience :: Developers",
18
+ "License :: OSI Approved :: GNU General Public License v3 (GPLv3)",
19
+ "Programming Language :: Python :: 3",
20
+ "Programming Language :: Python :: 3.12",
21
+ "Programming Language :: Python :: 3.13",
22
+ ]
23
+ dependencies = [
24
+ "aiohttp>=3.8.0",
25
+ ]
26
+
27
+ [project.optional-dependencies]
28
+
29
+
30
+ [tool.setuptools.packages.find]
31
+ where = ["."]
32
+ include = ["homevolt*"]
33
+
34
+ [tool.ruff]
35
+ line-length = 100
36
+ target-version = "py312"
37
+
38
+ [tool.ruff.lint]
39
+ select = ["E", "F", "I", "N", "W"]
40
+ ignore = ["E501"]
41
+
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+