Python API client for Indevolt devices
Project description
Indevolt API
Python client library for communicating with Indevolt devices (home battery systems).
Features
- Async/await support using aiohttp
- Fully typed with type hints
- Simple and intuitive API
- Comprehensive error handling
Installation
pip install indevolt-api
Quick Start
import asyncio
import aiohttp
from indevolt_api import (
IndevoltAPI,
IndevoltConfig,
IndevoltEnergyMode,
IndevoltSystem,
SET_REALTIME_ACTION,
IndevoltRealtimeAction,
)
async def main():
async with aiohttp.ClientSession() as session:
api = IndevoltAPI(host="192.168.1.100", port=8080, session=session)
# Get device configuration
config = await api.get_config()
print(f"Device config: {config}")
# Fetch data using StrEnum members — response keys are the same strings
data = await api.fetch_data([IndevoltConfig.READ_ENERGY_MODE, IndevoltSystem.INPUT_POWER])
print(f"Energy mode: {data[IndevoltConfig.READ_ENERGY_MODE]}")
print(f"Input power: {data[IndevoltSystem.INPUT_POWER]}")
# Write a single value
await api.set_data(IndevoltConfig.WRITE_DISCHARGE_LIMIT, 50)
# Write a real-time charge command
await api.set_data(SET_REALTIME_ACTION, [IndevoltRealtimeAction.CHARGE, 700, 80])
asyncio.run(main())
Device Discovery
The library supports two complementary discovery mechanisms.
Active Discovery
Sends a UDP broadcast and collects replies from devices on the same network. Use async_discover() when you need devices immediately at startup.
import asyncio
import aiohttp
from indevolt_api import async_discover, IndevoltAPI
async def main():
# Broadcast AT+IGDEVICEIP and wait for replies (default: 5 s)
devices = await async_discover()
if not devices:
print("No devices found")
return
print(f"Found {len(devices)} device(s):")
for device in devices:
print(f" - {device.host}:{device.port} (name: {device.name})")
# Connect to the first discovered device
async with aiohttp.ClientSession() as session:
api = IndevoltAPI.from_discovered_device(devices[0], session)
config = await api.get_config()
print(f"Device config: {config}")
asyncio.run(main())
How it works:
- Sends
ACTIVE_DISCOVERY_MESSAGE(AT+IGDEVICEIP) via UDP broadcast to255.255.255.255:8099 - Devices respond to local port
ACTIVE_DISCOVERY_PORT(10000) with their IP and optional metadata - Returns a list of
DiscoveredDeviceobjects
Note: UDP port 10000 must be available and not blocked by a firewall.
Passive Discovery
Listens for unsolicited broadcasts that devices emit on their own. Use PassiveDiscoveryProtocol for long-running applications (e.g. Home Assistant integrations) that need to detect devices as they appear without polling.
import asyncio
from indevolt_api import (
PassiveDiscoveryProtocol,
PASSIVE_DISCOVERY_PORT,
PASSIVE_DISCOVERY_BIND_ADDR,
)
async def main():
seen: set[str] = set()
def on_device_discovered(host: str) -> None:
if host not in seen:
seen.add(host)
print(f"Device announced itself: {host}")
loop = asyncio.get_running_loop()
transport, _ = await loop.create_datagram_endpoint(
lambda: PassiveDiscoveryProtocol(on_device_discovered),
local_addr=(PASSIVE_DISCOVERY_BIND_ADDR, PASSIVE_DISCOVERY_PORT),
)
try:
await asyncio.Event().wait() # run until cancelled
finally:
transport.close()
asyncio.run(main())
How it works:
- Devices periodically broadcast a
BCF-D-prefixed UDP packet on port8099 PassiveDiscoveryProtocolfilters packets by thePASSIVE_DISCOVERY_MAGICprefix and invokes your callback with the sender's IP- No outbound traffic is sent
Note: Bind to PASSIVE_DISCOVERY_BIND_ADDR (0.0.0.0) so the socket accepts broadcasts on all interfaces.
Discovery Examples
See examples/active_discovery_example.py and examples/passive_discovery_example.py for runnable examples.
API Reference
IndevoltAPI
__init__(host: str, port: int, session: aiohttp.ClientSession, timeout: float = 10.0)
Initialize the API client.
Parameters:
host(str): Device hostname or IP addressport(int): Device port number (typically 80 or 8080)session(aiohttp.ClientSession): An aiohttp client sessiontimeout(float): Request timeout in seconds (default: 10.0)
Example:
# Default 10-second timeout (recommended for local devices)
api = IndevoltAPI(host="192.168.1.100", port=8080, session=session)
# Custom timeout
api = IndevoltAPI(host="192.168.1.100", port=8080, session=session, timeout=15.0)
classmethod from_discovered_device(device: DiscoveredDevice, session: aiohttp.ClientSession, timeout: float = 10.0)
Create an API client from a discovered device.
Parameters:
device(DiscoveredDevice): A device object returned byasync_discover()session(aiohttp.ClientSession): An aiohttp client sessiontimeout(float): Request timeout in seconds (default: 10.0)
Returns:
- IndevoltAPI instance configured for the discovered device
Example:
devices = await async_discover()
if devices:
api = IndevoltAPI.from_discovered_device(devices[0], session)
async fetch_data(t: str | list[str]) -> dict[str, Any]
Fetch data from the device.
Parameters:
t: AStrEnummember, a raw string key, or a list of either
Returns:
- Dictionary whose keys are strings matching the requested cJson points.
StrEnummembers can be used directly to index the result.
Example:
from indevolt_api import IndevoltSystem, IndevoltGrid, IndevoltBattery
# Single point
data = await api.fetch_data(IndevoltBattery.SOC)
print(data[IndevoltBattery.SOC])
# Multiple points
data = await api.fetch_data([
IndevoltSystem.INPUT_POWER,
IndevoltSystem.OUTPUT_POWER,
IndevoltGrid.VOLTAGE,
])
print(data[IndevoltSystem.INPUT_POWER])
print(data[IndevoltGrid.VOLTAGE])
async set_data(t: str | int, v: Any) -> bool
Write data to the device.
Parameters:
t: cJson point identifier (e.g.,"47015"or47015)v: Value(s) to write (automatically converted to list of integers)
Returns:
Trueon success,Falseotherwise
Example:
from indevolt_api import IndevoltConfig, IndevoltEnergyMode, SET_REALTIME_ACTION, IndevoltRealtimeAction
# Single value
await api.set_data(IndevoltConfig.WRITE_DISCHARGE_LIMIT, 50)
# Real-time command with multiple values
await api.set_data(SET_REALTIME_ACTION, [IndevoltRealtimeAction.CHARGE, 700, 80])
# Set energy mode
await api.set_data(IndevoltConfig.WRITE_ENERGY_MODE, IndevoltEnergyMode.SELF_CONSUMED_PRIORITIZED)
async stop() -> bool
Stop any active real-time charge or discharge action.
Returns:
Trueon success,Falseif the command was rejected or a connection error occurred
Example:
succeeded = await api.stop()
async charge(power: int, target_soc: int) -> bool
Send a real-time charge command to the device.
Parameters:
power(int): Charge power in wattstarget_soc(int): Target state of charge percentage
Returns:
Trueon success,Falseif the command was rejected or a connection error occurred
Example:
succeeded = await api.charge(power=700, target_soc=80)
async discharge(power: int, target_soc: int) -> bool
Send a real-time discharge command to the device.
Parameters:
power(int): Discharge power in wattstarget_soc(int): Target state of charge percentage
Returns:
Trueon success,Falseif the command was rejected or a connection error occurred
Example:
succeeded = await api.discharge(power=400, target_soc=20)
async get_config() -> dict[str, Any]
Get system configuration from the device.
Returns:
- Dictionary with device system configuration
Raises:
TimeoutError: If the request exceeds the configured timeoutaiohttp.ClientError: On network errors or non-200 HTTP responses
Example:
config = await api.get_config()
print(config)
check_charge_limits(power: int, target_soc: int, generation: int) -> None
Check that charge parameters do not exceed device limits. Raises an exception if any boundary is violated.
Parameters:
power(int): Requested charge power in wattstarget_soc(int): Target state of charge percentagegeneration(int): Device hardware generation (1or2), available fromget_config()underdevice.generation
Raises:
PowerExceedsMaxError: Ifpowerexceeds the maximum for the given generationSocBelowMinimumError: Iftarget_socis below the minimum SOC (5%)
Example:
config = await api.get_config()
generation = config["device"]["generation"]
try:
api.check_charge_limits(power=1000, target_soc=80, generation=generation)
except PowerExceedsMaxError as e:
print(f"Power {e.power}W exceeds max {e.max_power}W for gen {e.generation}")
except SocBelowMinimumError as e:
print(f"Target SOC {e.target_soc}% is below minimum {e.minimum_soc}%")
check_discharge_limits(power: int, target_soc: int, generation: int) -> None
Check that discharge parameters do not exceed device limits. Raises an exception if any boundary is violated.
Parameters:
power(int): Requested discharge power in wattstarget_soc(int): Target state of charge percentagegeneration(int): Device hardware generation (1or2), available fromget_config()underdevice.generation
Raises:
PowerExceedsMaxError: Ifpowerexceeds the maximum for the given generationSocBelowMinimumError: Iftarget_socis below the minimum SOC (5%)
Example:
config = await api.get_config()
generation = config["device"]["generation"]
try:
api.check_discharge_limits(power=600, target_soc=10, generation=generation)
except PowerExceedsMaxError as e:
print(f"Power {e.power}W exceeds max {e.max_power}W for gen {e.generation}")
except SocBelowMinimumError as e:
print(f"Target SOC {e.target_soc}% is below minimum {e.minimum_soc}%")
async_discover(timeout: float = 5.0) -> list[DiscoveredDevice]
Discover Indevolt devices on the local network using UDP broadcast.
Parameters:
timeout(float): Discovery timeout in seconds (default: 5.0)
Returns:
- List of
DiscoveredDeviceobjects representing found devices
Example:
devices = await async_discover(timeout=3.0)
for device in devices:
print(f"Found: {device.host}:{device.port}")
DiscoveredDevice
Represents a discovered Indevolt device with the following attributes:
Attributes:
host(str): Device IP addressport(int): Device port number (default: 8080)name(str | None): Device name if provided in discovery responsemetadata(dict): Additional device information from discovery response
Example:
device = devices[0]
print(f"Device at {device.host}:{device.port}")
if device.name:
print(f"Name: {device.name}")
Exception Handling
The library has two exception-handling behaviours depending on the method:
Methods that raise (fetch_data, get_config): network and HTTP errors propagate to the caller.
Methods that return False (set_data, stop, charge, discharge): all network, HTTP, and decoding errors are caught internally and logged at DEBUG level. These methods never raise on communication failure.
TimeoutError
Built-in Python exception raised by fetch_data and get_config when a request exceeds the configured timeout (default: 10 seconds). Not raised by set_data or the convenience helpers.
aiohttp.ClientError
Raised by fetch_data and get_config on network errors or non-200 HTTP responses. Not raised by set_data or the convenience helpers.
PowerExceedsMaxError
Raised by check_charge_limits() or check_discharge_limits() when the requested power exceeds the device maximum.
Attributes: power, max_power, generation
SocBelowMinimumError
Raised by check_charge_limits() or check_discharge_limits() when the target SOC is below the hard minimum of 5%.
Attributes: target_soc, minimum_soc
Example:
import aiohttp
from indevolt_api import IndevoltAPI
# fetch_data and get_config raise on error
try:
data = await api.fetch_data("7101")
except TimeoutError:
print("Request timed out")
except aiohttp.ClientError as e:
print(f"Network/HTTP error: {e}")
# set_data and helpers return False on error — no try/except needed
succeeded = await api.set_data(IndevoltConfig.WRITE_DISCHARGE_LIMIT, 50)
if not succeeded:
print("Command failed")
Note: You can adjust the timeout when creating the API client:
# Increase timeout if needed (e.g., for slower networks)
api = IndevoltAPI(host="192.168.1.100", port=8080, session=session, timeout=10.0)
Constants and Enums
All register keys and action values are available as typed StrEnum classes, importable directly from indevolt_api. Because StrEnum members are strings, they can be passed directly to fetch_data() and set_data(), and used to index the response dictionary — no manual conversion needed.
IndevoltConfig
Register keys for configurable device settings (read and write).
from indevolt_api import IndevoltConfig
# Write registers
IndevoltConfig.WRITE_ENERGY_MODE # "47005"
IndevoltConfig.WRITE_DISCHARGE_LIMIT # "1142"
# ... and more
# Read registers
IndevoltConfig.READ_ENERGY_MODE # "7101"
IndevoltConfig.READ_DISCHARGE_LIMIT # "6105"
# ... and more
IndevoltRealtimeAction
Action values for real-time control mode, used with SET_REALTIME_ACTION.
from indevolt_api import IndevoltRealtimeAction
IndevoltRealtimeAction.STOP # "0"
IndevoltRealtimeAction.CHARGE # "1"
IndevoltRealtimeAction.DISCHARGE # "2"
IndevoltEnergyMode
Energy mode values for IndevoltConfig.WRITE_ENERGY_MODE.
from indevolt_api import IndevoltEnergyMode
IndevoltEnergyMode.OUTDOOR_PORTABLE
IndevoltEnergyMode.SELF_CONSUMED_PRIORITIZED
IndevoltEnergyMode.REAL_TIME_CONTROL
IndevoltEnergyMode.CHARGE_DISCHARGE_SCHEDULE
IndevoltBattery, IndevoltSystem, IndevoltGrid, IndevoltSolar
Register key enums for reading battery, system-level, grid, and solar data points.
from indevolt_api import IndevoltBattery, IndevoltSystem, IndevoltGrid, IndevoltSolar
data = await api.fetch_data([
IndevoltBattery.SOC,
IndevoltBattery.POWER,
IndevoltSystem.OUTPUT_POWER,
IndevoltGrid.METER_POWER_GEN2,
IndevoltSolar.DC_INPUT_POWER_1,
])
SET_REALTIME_ACTION
The register key used to send real-time charge/discharge commands to the device.
from indevolt_api import SET_REALTIME_ACTION
await api.set_data(SET_REALTIME_ACTION, [IndevoltRealtimeAction.CHARGE, 700, 80])
Discovery Constants
All discovery-related constants are importable from indevolt_api.
| Constant | Value | Description |
|---|---|---|
ACTIVE_DISCOVERY_PORT |
10000 |
Local port devices respond to |
ACTIVE_DISCOVERY_MESSAGE |
b"AT+IGDEVICEIP" |
Broadcast payload |
ACTIVE_DISCOVERY_TIMEOUT |
5.0 |
Default async_discover timeout (seconds) |
PASSIVE_DISCOVERY_PORT |
8099 |
Port to bind for passive listening |
PASSIVE_DISCOVERY_MAGIC |
b"BCF-D" |
Magic prefix of device broadcasts |
PASSIVE_DISCOVERY_BIND_ADDR |
"0.0.0.0" |
Bind address for the passive listener |
DEVICE_LIMITS
Dictionary of per-generation device limits used by check_charge_limits() and check_discharge_limits().
from indevolt_api import DEVICE_LIMITS
print(DEVICE_LIMITS[1]) # {'max_discharge_power': 800, 'max_charge_power': 1200, 'minimum_soc': 5}
print(DEVICE_LIMITS[2]) # {'max_discharge_power': 2400, 'max_charge_power': 2400, 'minimum_soc': 5}
Requirements
- Python 3.11+
- aiohttp >= 3.9.0
License
MIT License - see LICENSE file for details
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file indevolt_api-1.7.2.tar.gz.
File metadata
- Download URL: indevolt_api-1.7.2.tar.gz
- Upload date:
- Size: 15.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.11.15
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
989b63e27a28f0a3e53115288fc7c68aafc5c21623bd6d112ae6c063182d3e8f
|
|
| MD5 |
6c087c2618d8d3d45a312acd694ae9f4
|
|
| BLAKE2b-256 |
ae358410e68cf67fec1eddc780ecec27f381c80fc08751868c06e17df3cc53a5
|
File details
Details for the file indevolt_api-1.7.2-py3-none-any.whl.
File metadata
- Download URL: indevolt_api-1.7.2-py3-none-any.whl
- Upload date:
- Size: 13.8 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/6.2.0 CPython/3.11.15
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
710f2b214df898cc3ea02a268416ef04a23f968d3330cc85f29de560a4066c21
|
|
| MD5 |
c0635b44d2a13030c2fb8a127210b11f
|
|
| BLAKE2b-256 |
886746e9bd0e6c151651f0d343b8b6244e662a3a270a35f17e3aa573505c9264
|