Homevolt 0.1.0__py3-none-any.whl → 0.2.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 CHANGED
@@ -5,7 +5,7 @@ from .exceptions import (
5
5
  HomevoltAuthenticationError,
6
6
  HomevoltConnectionError,
7
7
  HomevoltDataError,
8
- HomevoltException,
8
+ HomevoltError,
9
9
  )
10
10
  from .homevolt import Homevolt
11
11
  from .models import DeviceMetadata, Sensor, SensorType
@@ -17,8 +17,7 @@ __all__ = [
17
17
  "HomevoltAuthenticationError",
18
18
  "HomevoltConnectionError",
19
19
  "HomevoltDataError",
20
- "HomevoltException",
20
+ "HomevoltError",
21
21
  "Sensor",
22
22
  "SensorType",
23
23
  ]
24
-
homevolt/const.py CHANGED
@@ -3,14 +3,20 @@
3
3
  # API endpoints
4
4
  ENDPOINT_EMS = "/ems.json"
5
5
  ENDPOINT_SCHEDULE = "/schedule.json"
6
+ ENDPOINT_CONSOLE = "/console.json"
7
+ ENDPOINT_PARAMS = "/params.json"
6
8
 
7
9
  SCHEDULE_TYPE = {
8
10
  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",
11
+ 1: "Inverter Charge",
12
+ 2: "Inverter Discharge",
13
+ 3: "Grid Charge",
14
+ 4: "Grid Discharge",
15
+ 5: "Grid Charge/Discharge",
16
+ 6: "Frequency Reserve",
17
+ 7: "Solar Charge",
18
+ 8: "Solar Charge/Discharge",
19
+ 9: "Full Solar Export",
14
20
  }
15
21
 
16
22
  # Device type mappings for sensors
@@ -20,4 +26,3 @@ DEVICE_MAP = {
20
26
  "load": "load",
21
27
  "house": "load",
22
28
  }
23
-
homevolt/device.py CHANGED
@@ -7,7 +7,14 @@ from typing import Any
7
7
 
8
8
  import aiohttp
9
9
 
10
- from .const import DEVICE_MAP, ENDPOINT_EMS, ENDPOINT_SCHEDULE, SCHEDULE_TYPE
10
+ from .const import (
11
+ DEVICE_MAP,
12
+ ENDPOINT_CONSOLE,
13
+ ENDPOINT_EMS,
14
+ ENDPOINT_PARAMS,
15
+ ENDPOINT_SCHEDULE,
16
+ SCHEDULE_TYPE,
17
+ )
11
18
  from .exceptions import (
12
19
  HomevoltAuthenticationError,
13
20
  HomevoltConnectionError,
@@ -210,26 +217,54 @@ class Device:
210
217
  name=f"Homevolt Battery {bat_id}",
211
218
  model="Homevolt Battery",
212
219
  )
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
- )
220
+ if "soc" in battery:
221
+ self.sensors[f"Homevolt battery {bat_id}"] = Sensor(
222
+ value=battery["soc"] / 100,
223
+ type=SensorType.PERCENTAGE,
224
+ device_identifier=battery_device_id,
225
+ )
226
+ if "tmin" in battery:
227
+ self.sensors[f"Homevolt battery {bat_id} tmin"] = Sensor(
228
+ value=battery["tmin"] / 10,
229
+ type=SensorType.TEMPERATURE,
230
+ device_identifier=battery_device_id,
231
+ )
232
+ if "tmax" in battery:
233
+ self.sensors[f"Homevolt battery {bat_id} tmax"] = Sensor(
234
+ value=battery["tmax"] / 10,
235
+ type=SensorType.TEMPERATURE,
236
+ device_identifier=battery_device_id,
237
+ )
238
+ if "cycle_count" in battery:
239
+ self.sensors[f"Homevolt battery {bat_id} charge cycles"] = Sensor(
240
+ value=battery["cycle_count"],
241
+ type=SensorType.COUNT,
242
+ device_identifier=battery_device_id,
243
+ )
244
+ if "voltage" in battery:
245
+ self.sensors[f"Homevolt battery {bat_id} voltage"] = Sensor(
246
+ value=battery["voltage"] / 100,
247
+ type=SensorType.VOLTAGE,
248
+ device_identifier=battery_device_id,
249
+ )
250
+ if "current" in battery:
251
+ self.sensors[f"Homevolt battery {bat_id} current"] = Sensor(
252
+ value=battery["current"],
253
+ type=SensorType.CURRENT,
254
+ device_identifier=battery_device_id,
255
+ )
256
+ if "power" in battery:
257
+ self.sensors[f"Homevolt battery {bat_id} power"] = Sensor(
258
+ value=battery["power"],
259
+ type=SensorType.POWER,
260
+ device_identifier=battery_device_id,
261
+ )
262
+ if "soh" in battery:
263
+ self.sensors[f"Homevolt battery {bat_id} soh"] = Sensor(
264
+ value=battery["soh"] / 100,
265
+ type=SensorType.PERCENTAGE,
266
+ device_identifier=battery_device_id,
267
+ )
233
268
 
234
269
  # External sensors (grid, solar, load)
235
270
  for sensor in ems_data.get("sensors", []):
@@ -304,7 +339,11 @@ class Device:
304
339
  device_identifier=ems_device_id,
305
340
  )
306
341
 
307
- schedule = schedule_data.get("schedule", [{}])[0] if schedule_data.get("schedule") else {"type": -1, "params": {}}
342
+ schedule = (
343
+ schedule_data.get("schedule", [{}])[0]
344
+ if schedule_data.get("schedule")
345
+ else {"type": -1, "params": {}}
346
+ )
308
347
 
309
348
  self.sensors["Schedule Type"] = Sensor(
310
349
  value=SCHEDULE_TYPE.get(schedule.get("type", -1)),
@@ -327,3 +366,395 @@ class Device:
327
366
  device_identifier=ems_device_id,
328
367
  )
329
368
 
369
+ async def _execute_console_command(self, command: str) -> dict[str, Any]:
370
+ """Execute a console command via the HTTP API.
371
+
372
+ Args:
373
+ command: The console command to execute
374
+
375
+ Returns:
376
+ The JSON response from the console endpoint
377
+
378
+ Raises:
379
+ HomevoltConnectionError: If connection fails
380
+ HomevoltAuthenticationError: If authentication fails
381
+ HomevoltDataError: If response parsing fails
382
+ """
383
+ try:
384
+ url = f"http://{self._ip_address}{ENDPOINT_CONSOLE}"
385
+ async with self._websession.post(
386
+ url,
387
+ auth=self._auth,
388
+ json={"cmd": command},
389
+ ) as response:
390
+ if response.status == 401:
391
+ raise HomevoltAuthenticationError("Authentication failed")
392
+ response.raise_for_status()
393
+ return await response.json()
394
+ except aiohttp.ClientError as err:
395
+ raise HomevoltConnectionError(f"Failed to execute command: {err}") from err
396
+ except Exception as err:
397
+ raise HomevoltDataError(f"Failed to parse command response: {err}") from err
398
+
399
+ async def set_battery_mode(
400
+ self,
401
+ mode: int,
402
+ *,
403
+ setpoint: int | None = None,
404
+ max_charge: int | None = None,
405
+ max_discharge: int | None = None,
406
+ min_soc: int | None = None,
407
+ max_soc: int | None = None,
408
+ offline: bool = False,
409
+ ) -> dict[str, Any]:
410
+ """Set immediate battery control mode.
411
+
412
+ Args:
413
+ mode: Schedule type (0=Idle, 1=Inverter Charge, 2=Inverter Discharge,
414
+ 3=Grid Charge, 4=Grid Discharge, 5=Grid Charge/Discharge,
415
+ 6=Frequency Reserve, 7=Solar Charge, 8=Solar Charge/Discharge,
416
+ 9=Full Solar Export)
417
+ setpoint: Power setpoint in Watts (for grid modes)
418
+ max_charge: Maximum charge power in Watts
419
+ max_discharge: Maximum discharge power in Watts
420
+ min_soc: Minimum state of charge percentage
421
+ max_soc: Maximum state of charge percentage
422
+ offline: Take inverter offline during idle mode
423
+
424
+ Returns:
425
+ Response from the console command
426
+
427
+ Raises:
428
+ HomevoltConnectionError: If connection fails
429
+ HomevoltAuthenticationError: If authentication fails
430
+ HomevoltDataError: If command execution fails
431
+ """
432
+ if mode not in SCHEDULE_TYPE:
433
+ raise ValueError(f"Invalid mode: {mode}. Must be 0-9")
434
+
435
+ cmd_parts = [f"sched_set {mode}"]
436
+
437
+ if setpoint is not None:
438
+ cmd_parts.append(f"-s {setpoint}")
439
+ if max_charge is not None:
440
+ cmd_parts.append(f"-c {max_charge}")
441
+ if max_discharge is not None:
442
+ cmd_parts.append(f"-d {max_discharge}")
443
+ if min_soc is not None:
444
+ cmd_parts.append(f"--min {min_soc}")
445
+ if max_soc is not None:
446
+ cmd_parts.append(f"--max {max_soc}")
447
+ if offline:
448
+ cmd_parts.append("-o")
449
+
450
+ command = " ".join(cmd_parts)
451
+ return await self._execute_console_command(command)
452
+
453
+ async def add_schedule(
454
+ self,
455
+ mode: int,
456
+ *,
457
+ from_time: str | None = None,
458
+ to_time: str | None = None,
459
+ setpoint: int | None = None,
460
+ max_charge: int | None = None,
461
+ max_discharge: int | None = None,
462
+ min_soc: int | None = None,
463
+ max_soc: int | None = None,
464
+ offline: bool = False,
465
+ ) -> dict[str, Any]:
466
+ """Add a scheduled battery control entry.
467
+
468
+ Args:
469
+ mode: Schedule type (0=Idle, 1=Inverter Charge, 2=Inverter Discharge,
470
+ 3=Grid Charge, 4=Grid Discharge, 5=Grid Charge/Discharge,
471
+ 6=Frequency Reserve, 7=Solar Charge, 8=Solar Charge/Discharge,
472
+ 9=Full Solar Export)
473
+ from_time: Start time in ISO format (YYYY-MM-DDTHH:mm:ss)
474
+ to_time: End time in ISO format (YYYY-MM-DDTHH:mm:ss)
475
+ setpoint: Power setpoint in Watts (for grid modes)
476
+ max_charge: Maximum charge power in Watts
477
+ max_discharge: Maximum discharge power in Watts
478
+ min_soc: Minimum state of charge percentage
479
+ max_soc: Maximum state of charge percentage
480
+ offline: Take inverter offline during idle mode
481
+
482
+ Returns:
483
+ Response from the console command
484
+
485
+ Raises:
486
+ HomevoltConnectionError: If connection fails
487
+ HomevoltAuthenticationError: If authentication fails
488
+ HomevoltDataError: If command execution fails
489
+ """
490
+ if mode not in SCHEDULE_TYPE:
491
+ raise ValueError(f"Invalid mode: {mode}. Must be 0-9")
492
+
493
+ cmd_parts = [f"sched_add {mode}"]
494
+
495
+ if from_time:
496
+ cmd_parts.append(f"--from {from_time}")
497
+ if to_time:
498
+ cmd_parts.append(f"--to {to_time}")
499
+ if setpoint is not None:
500
+ cmd_parts.append(f"-s {setpoint}")
501
+ if max_charge is not None:
502
+ cmd_parts.append(f"-c {max_charge}")
503
+ if max_discharge is not None:
504
+ cmd_parts.append(f"-d {max_discharge}")
505
+ if min_soc is not None:
506
+ cmd_parts.append(f"--min {min_soc}")
507
+ if max_soc is not None:
508
+ cmd_parts.append(f"--max {max_soc}")
509
+ if offline:
510
+ cmd_parts.append("-o")
511
+
512
+ command = " ".join(cmd_parts)
513
+ return await self._execute_console_command(command)
514
+
515
+ async def delete_schedule(self, schedule_id: int) -> dict[str, Any]:
516
+ """Delete a schedule by ID.
517
+
518
+ Args:
519
+ schedule_id: The ID of the schedule to delete
520
+
521
+ Returns:
522
+ Response from the console command
523
+
524
+ Raises:
525
+ HomevoltConnectionError: If connection fails
526
+ HomevoltAuthenticationError: If authentication fails
527
+ HomevoltDataError: If command execution fails
528
+ """
529
+ return await self._execute_console_command(f"sched_del {schedule_id}")
530
+
531
+ async def clear_all_schedules(self) -> dict[str, Any]:
532
+ """Clear all schedules.
533
+
534
+ Returns:
535
+ Response from the console command
536
+
537
+ Raises:
538
+ HomevoltConnectionError: If connection fails
539
+ HomevoltAuthenticationError: If authentication fails
540
+ HomevoltDataError: If command execution fails
541
+ """
542
+ return await self._execute_console_command("sched_clear")
543
+
544
+ async def enable_local_mode(self) -> dict[str, Any]:
545
+ """Enable local mode to prevent remote schedule overrides.
546
+
547
+ When enabled, remote schedules from Tibber/partners via MQTT will be blocked,
548
+ and only local schedules will be used.
549
+
550
+ Returns:
551
+ Response from the params endpoint
552
+
553
+ Raises:
554
+ HomevoltConnectionError: If connection fails
555
+ HomevoltAuthenticationError: If authentication fails
556
+ HomevoltDataError: If parameter setting fails
557
+ """
558
+ return await self.set_parameter("settings_local", 1)
559
+
560
+ async def disable_local_mode(self) -> dict[str, Any]:
561
+ """Disable local mode to allow remote schedule overrides.
562
+
563
+ When disabled, remote schedules from Tibber/partners via MQTT will replace
564
+ local schedules.
565
+
566
+ Returns:
567
+ Response from the params endpoint
568
+
569
+ Raises:
570
+ HomevoltConnectionError: If connection fails
571
+ HomevoltAuthenticationError: If authentication fails
572
+ HomevoltDataError: If parameter setting fails
573
+ """
574
+ return await self.set_parameter("settings_local", 0)
575
+
576
+ async def set_parameter(self, key: str, value: Any) -> dict[str, Any]:
577
+ """Set a device parameter.
578
+
579
+ Args:
580
+ key: Parameter name
581
+ value: Parameter value
582
+
583
+ Returns:
584
+ Response from the params endpoint
585
+
586
+ Raises:
587
+ HomevoltConnectionError: If connection fails
588
+ HomevoltAuthenticationError: If authentication fails
589
+ HomevoltDataError: If parameter setting fails
590
+ """
591
+ try:
592
+ url = f"http://{self._ip_address}{ENDPOINT_PARAMS}"
593
+ async with self._websession.post(
594
+ url,
595
+ auth=self._auth,
596
+ json={key: value},
597
+ ) as response:
598
+ if response.status == 401:
599
+ raise HomevoltAuthenticationError("Authentication failed")
600
+ response.raise_for_status()
601
+ return await response.json()
602
+ except aiohttp.ClientError as err:
603
+ raise HomevoltConnectionError(f"Failed to set parameter: {err}") from err
604
+ except Exception as err:
605
+ raise HomevoltDataError(f"Failed to parse parameter response: {err}") from err
606
+
607
+ async def get_parameter(self, key: str) -> Any:
608
+ """Get a device parameter value.
609
+
610
+ Args:
611
+ key: Parameter name
612
+
613
+ Returns:
614
+ Parameter value
615
+
616
+ Raises:
617
+ HomevoltConnectionError: If connection fails
618
+ HomevoltAuthenticationError: If authentication fails
619
+ HomevoltDataError: If parameter retrieval fails
620
+ """
621
+ try:
622
+ url = f"http://{self._ip_address}{ENDPOINT_PARAMS}"
623
+ async with self._websession.get(url, auth=self._auth) as response:
624
+ if response.status == 401:
625
+ raise HomevoltAuthenticationError("Authentication failed")
626
+ response.raise_for_status()
627
+ params = await response.json()
628
+ return params.get(key)
629
+ except aiohttp.ClientError as err:
630
+ raise HomevoltConnectionError(f"Failed to get parameter: {err}") from err
631
+ except Exception as err:
632
+ raise HomevoltDataError(f"Failed to parse parameter response: {err}") from err
633
+
634
+ async def charge_battery(
635
+ self,
636
+ *,
637
+ max_power: int | None = None,
638
+ max_soc: int | None = None,
639
+ min_soc: int | None = None,
640
+ ) -> dict[str, Any]:
641
+ """Charge battery using inverter (immediate).
642
+
643
+ Args:
644
+ max_power: Maximum charge power in Watts
645
+ max_soc: Maximum state of charge percentage (stops at this level)
646
+ min_soc: Minimum state of charge percentage (only charges if below this)
647
+
648
+ Returns:
649
+ Response from the console command
650
+ """
651
+ return await self.set_battery_mode(
652
+ 1, # Inverter Charge
653
+ max_charge=max_power,
654
+ max_soc=max_soc,
655
+ min_soc=min_soc,
656
+ )
657
+
658
+ async def discharge_battery(
659
+ self,
660
+ *,
661
+ max_power: int | None = None,
662
+ min_soc: int | None = None,
663
+ max_soc: int | None = None,
664
+ ) -> dict[str, Any]:
665
+ """Discharge battery using inverter (immediate).
666
+
667
+ Args:
668
+ max_power: Maximum discharge power in Watts
669
+ min_soc: Minimum state of charge percentage (stops at this level)
670
+ max_soc: Maximum state of charge percentage (only discharges if above this)
671
+
672
+ Returns:
673
+ Response from the console command
674
+ """
675
+ return await self.set_battery_mode(
676
+ 2, # Inverter Discharge
677
+ max_discharge=max_power,
678
+ min_soc=min_soc,
679
+ max_soc=max_soc,
680
+ )
681
+
682
+ async def set_battery_idle(self, *, offline: bool = False) -> dict[str, Any]:
683
+ """Set battery to idle mode (immediate).
684
+
685
+ Args:
686
+ offline: If True, take inverter offline during idle
687
+
688
+ Returns:
689
+ Response from the console command
690
+ """
691
+ return await self.set_battery_mode(0, offline=offline)
692
+
693
+ async def charge_from_grid(
694
+ self,
695
+ *,
696
+ setpoint: int,
697
+ max_power: int | None = None,
698
+ max_soc: int | None = None,
699
+ ) -> dict[str, Any]:
700
+ """Charge battery from grid with power setpoint (immediate).
701
+
702
+ Args:
703
+ setpoint: Power setpoint in Watts
704
+ max_power: Maximum charge power in Watts
705
+ max_soc: Maximum state of charge percentage
706
+
707
+ Returns:
708
+ Response from the console command
709
+ """
710
+ return await self.set_battery_mode(
711
+ 3, # Grid Charge
712
+ setpoint=setpoint,
713
+ max_charge=max_power,
714
+ max_soc=max_soc,
715
+ )
716
+
717
+ async def discharge_to_grid(
718
+ self,
719
+ *,
720
+ setpoint: int,
721
+ max_power: int | None = None,
722
+ min_soc: int | None = None,
723
+ ) -> dict[str, Any]:
724
+ """Discharge battery to grid with power setpoint (immediate).
725
+
726
+ Args:
727
+ setpoint: Power setpoint in Watts
728
+ max_power: Maximum discharge power in Watts
729
+ min_soc: Minimum state of charge percentage
730
+
731
+ Returns:
732
+ Response from the console command
733
+ """
734
+ return await self.set_battery_mode(
735
+ 4, # Grid Discharge
736
+ setpoint=setpoint,
737
+ max_discharge=max_power,
738
+ min_soc=min_soc,
739
+ )
740
+
741
+ async def charge_from_solar(
742
+ self,
743
+ *,
744
+ max_power: int | None = None,
745
+ max_soc: int | None = None,
746
+ ) -> dict[str, Any]:
747
+ """Charge battery from solar only (immediate).
748
+
749
+ Args:
750
+ max_power: Maximum charge power in Watts
751
+ max_soc: Maximum state of charge percentage
752
+
753
+ Returns:
754
+ Response from the console command
755
+ """
756
+ return await self.set_battery_mode(
757
+ 7, # Solar Charge
758
+ max_charge=max_power,
759
+ max_soc=max_soc,
760
+ )
homevolt/exceptions.py CHANGED
@@ -1,26 +1,25 @@
1
1
  """Custom exceptions for the Homevolt library."""
2
2
 
3
3
 
4
- class HomevoltException(Exception):
4
+ class HomevoltError(Exception):
5
5
  """Base exception for all Homevolt errors."""
6
6
 
7
7
  pass
8
8
 
9
9
 
10
- class HomevoltConnectionError(HomevoltException):
10
+ class HomevoltConnectionError(HomevoltError):
11
11
  """Raised when there's a connection or network error."""
12
12
 
13
13
  pass
14
14
 
15
15
 
16
- class HomevoltAuthenticationError(HomevoltException):
16
+ class HomevoltAuthenticationError(HomevoltError):
17
17
  """Raised when authentication fails."""
18
18
 
19
19
  pass
20
20
 
21
21
 
22
- class HomevoltDataError(HomevoltException):
22
+ class HomevoltDataError(HomevoltError):
23
23
  """Raised when there's an error parsing or processing data."""
24
24
 
25
25
  pass
26
-
homevolt/homevolt.py CHANGED
@@ -38,6 +38,7 @@ class Homevolt:
38
38
  """Fetch and update device information."""
39
39
  if self._device is None:
40
40
  await self._ensure_session()
41
+ assert self._websession is not None
41
42
  self._device = Device(
42
43
  ip_address=self._ip_address,
43
44
  password=self._password,
@@ -79,4 +80,3 @@ class Homevolt:
79
80
  async def __aexit__(self, exc_type, exc_val, exc_tb) -> None:
80
81
  """Async context manager exit."""
81
82
  await self.close_connection()
82
-
homevolt/models.py CHANGED
@@ -40,4 +40,3 @@ class Sensor:
40
40
  value: float | str | None
41
41
  type: SensorType
42
42
  device_identifier: str = "main" # Device identifier for grouping sensors into devices
43
-
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: Homevolt
3
- Version: 0.1.0
3
+ Version: 0.2.0
4
4
  Summary: Python library for Homevolt EMS devices
5
5
  Author-email: Your Name <your.email@example.com>
6
6
  License: GPL-3.0
@@ -24,6 +24,12 @@ Get real-time data from your Homevolt Energy Management System, including:
24
24
  - Grid, solar, and load sensor data
25
25
  - Schedule information
26
26
 
27
+ Control your battery with:
28
+ - Immediate battery control (charge, discharge, idle)
29
+ - Scheduled battery operations
30
+ - Local mode management
31
+ - Parameter configuration
32
+
27
33
  ## Install
28
34
 
29
35
  ```bash
@@ -46,20 +52,20 @@ async def main():
46
52
  websession=session,
47
53
  )
48
54
  await homevolt_connection.update_info()
49
-
55
+
50
56
  device = homevolt_connection.get_device()
51
57
  print(f"Device ID: {device.device_id}")
52
58
  print(f"Current Power: {device.sensors['Power'].value} W")
53
59
  print(f"Battery SOC: {device.sensors['Battery State of Charge'].value * 100}%")
54
-
60
+
55
61
  # Access all sensors
56
62
  for sensor_name, sensor in device.sensors.items():
57
63
  print(f"{sensor_name}: {sensor.value} ({sensor.type.value})")
58
-
64
+
59
65
  # Access device metadata
60
66
  for device_id, metadata in device.device_metadata.items():
61
67
  print(f"{device_id}: {metadata.name} ({metadata.model})")
62
-
68
+
63
69
  await homevolt_connection.close_connection()
64
70
 
65
71
 
@@ -83,10 +89,10 @@ async def main():
83
89
  websession=session,
84
90
  ) as homevolt_connection:
85
91
  await homevolt_connection.update_info()
86
-
92
+
87
93
  device = homevolt_connection.get_device()
88
94
  await device.update_info() # Refresh data
89
-
95
+
90
96
  print(f"Device ID: {device.device_id}")
91
97
  print(f"Available sensors: {list(device.sensors.keys())}")
92
98
 
@@ -95,6 +101,76 @@ if __name__ == "__main__":
95
101
  asyncio.run(main())
96
102
  ```
97
103
 
104
+ ## Battery Control Example
105
+
106
+ ```python
107
+ import asyncio
108
+ import aiohttp
109
+ import homevolt
110
+
111
+
112
+ async def main():
113
+ async with aiohttp.ClientSession() as session:
114
+ async with homevolt.Homevolt(
115
+ ip_address="192.168.1.100",
116
+ password="optional_password",
117
+ websession=session,
118
+ ) as homevolt_connection:
119
+ await homevolt_connection.update_info()
120
+ device = homevolt_connection.get_device()
121
+
122
+ # Enable local mode to prevent remote schedule overrides
123
+ await device.enable_local_mode()
124
+
125
+ # Charge battery immediately (up to 3000W, stop at 90% SOC)
126
+ await device.charge_battery(max_power=3000, max_soc=90)
127
+
128
+ # Or use the full control method
129
+ await device.set_battery_mode(
130
+ mode=1, # Inverter Charge
131
+ max_charge=3000,
132
+ max_soc=90,
133
+ )
134
+
135
+ # Schedule night charging (11 PM - 7 AM)
136
+ from datetime import datetime, timedelta
137
+ tonight = datetime.now().replace(hour=23, minute=0, second=0)
138
+ tomorrow = (tonight + timedelta(days=1)).replace(hour=7, minute=0, second=0)
139
+
140
+ await device.add_schedule(
141
+ mode=1, # Inverter Charge
142
+ from_time=tonight.isoformat(),
143
+ to_time=tomorrow.isoformat(),
144
+ max_charge=3000,
145
+ max_soc=80,
146
+ )
147
+
148
+ # Set battery to idle
149
+ await device.set_battery_idle()
150
+
151
+ # Discharge during peak hours
152
+ await device.discharge_battery(max_power=2500, min_soc=30)
153
+
154
+
155
+ if __name__ == "__main__":
156
+ asyncio.run(main())
157
+ ```
158
+
159
+ ## Battery Control Modes
160
+
161
+ The following modes are available for battery control:
162
+
163
+ - `0`: Idle - Battery standby (no charge/discharge)
164
+ - `1`: Inverter Charge - Charge battery via inverter from grid/solar
165
+ - `2`: Inverter Discharge - Discharge battery via inverter to home/grid
166
+ - `3`: Grid Charge - Charge from grid with power setpoint
167
+ - `4`: Grid Discharge - Discharge to grid with power setpoint
168
+ - `5`: Grid Charge/Discharge - Bidirectional grid control
169
+ - `6`: Frequency Reserve - Frequency regulation service mode
170
+ - `7`: Solar Charge - Charge from solar production only
171
+ - `8`: Solar Charge/Discharge - Solar-based grid management
172
+ - `9`: Full Solar Export - Export all solar production
173
+
98
174
  ## API Reference
99
175
 
100
176
  ### Homevolt
@@ -132,6 +208,28 @@ Represents a Homevolt EMS device.
132
208
  - `async fetch_ems_data()`: Fetch EMS data specifically
133
209
  - `async fetch_schedule_data()`: Fetch schedule data specifically
134
210
 
211
+ #### Battery Control Methods
212
+
213
+ **Immediate Control:**
214
+ - `async set_battery_mode(mode, **kwargs)`: Set immediate battery control mode
215
+ - `async charge_battery(**kwargs)`: Charge battery using inverter
216
+ - `async discharge_battery(**kwargs)`: Discharge battery using inverter
217
+ - `async set_battery_idle(**kwargs)`: Set battery to idle mode
218
+ - `async charge_from_grid(**kwargs)`: Charge from grid with power setpoint
219
+ - `async discharge_to_grid(**kwargs)`: Discharge to grid with power setpoint
220
+ - `async charge_from_solar(**kwargs)`: Charge from solar only
221
+
222
+ **Scheduled Control:**
223
+ - `async add_schedule(mode, **kwargs)`: Add a scheduled battery control entry
224
+ - `async delete_schedule(schedule_id)`: Delete a schedule by ID
225
+ - `async clear_all_schedules()`: Clear all schedules
226
+
227
+ **Configuration:**
228
+ - `async enable_local_mode()`: Enable local mode (prevents remote overrides)
229
+ - `async disable_local_mode()`: Disable local mode (allows remote overrides)
230
+ - `async set_parameter(key, value)`: Set a device parameter
231
+ - `async get_parameter(key)`: Get a device parameter value
232
+
135
233
  ### Data Models
136
234
 
137
235
  #### Sensor
@@ -163,7 +261,7 @@ Enumeration of sensor types:
163
261
 
164
262
  ### Exceptions
165
263
 
166
- - `HomevoltException`: Base exception for all Homevolt errors
264
+ - `HomevoltError`: Base exception for all Homevolt errors
167
265
  - `HomevoltConnectionError`: Connection or network errors
168
266
  - `HomevoltAuthenticationError`: Authentication failures
169
267
  - `HomevoltDataError`: Data parsing errors
@@ -0,0 +1,10 @@
1
+ homevolt/__init__.py,sha256=HAK08jrG9KyS7Nd3tJT4QRhmx3a3AY4tlh_szh_09cg,504
2
+ homevolt/const.py,sha256=hpiyo292WKIOfULnaJVY7n7afkn5cSQtqQFhQlQBpa4,609
3
+ homevolt/device.py,sha256=8WYdKfRIADr74t1LtFTlrainq7guaS1xXlCzJ0qwDs4,28362
4
+ homevolt/exceptions.py,sha256=TeG1J92JsbBpJok6Qpbdu4ENm9OjVMNgeHPcUoYjzdw,488
5
+ homevolt/homevolt.py,sha256=tRvXZvTrOixV3mFT3gkVNWfFiYLmVoDk1TXsHdUNj5o,2515
6
+ homevolt/models.py,sha256=YrzTuAsFbfG4c-XhZMYtIZAe6qhEsDmZZdL5zPLHnOA,928
7
+ homevolt-0.2.0.dist-info/METADATA,sha256=khhm9i6bcr5WrUDF-p8ev3JtKxg7LjSTb4q3N_x0qiU,8064
8
+ homevolt-0.2.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
9
+ homevolt-0.2.0.dist-info/top_level.txt,sha256=eOPhiXXDJEiBEdExWtFc_UqWSnbLbFpEsMm3fCXrBTA,9
10
+ homevolt-0.2.0.dist-info/RECORD,,
@@ -1,10 +0,0 @@
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,,