Compare commits

...

8 Commits

Author SHA1 Message Date
suaveolent
d8dd357488 add support for meters 2024-09-13 11:10:03 +02:00
suaveolent
413886b8b0 add identify_meters call 2024-09-13 11:10:03 +02:00
suaveolent
c9aa65bf37 update readme 2024-09-13 11:10:03 +02:00
suaveolent
5bfecfdfc6 update readme 2024-09-13 11:10:03 +02:00
suaveolent
523b84132a version bump 2024-09-13 11:10:03 +02:00
suaveolent
1fae169c29 add local_addr parameter to allow to bind to specific interface 2024-09-13 11:10:03 +02:00
suaveolent
be21769efc fix as json for identitfy requests, fixes #19 2024-09-13 11:10:03 +02:00
suaveolent
d0c296b556 fixes #55 2024-09-13 11:10:03 +02:00
5 changed files with 61 additions and 26 deletions

View File

@ -2,7 +2,7 @@
This Python library facilitates communication with Hoymiles DTUs and the HMS-XXXXW-2T HMS microinverters, utilizing protobuf messages.
For the Home Assistant integration have a look here:
For the Home Assistant integration have a look here:
https://github.com/suaveolent/ha-hoymiles-wifi
**Disclaimer: This library is not affiliated with Hoymiles. It is an independent project developed to provide tools for interacting with Hoymiles HMS-XXXXW-2T series micro-inverters featuring integrated WiFi DTU. Any trademarks or product names mentioned are the property of their respective owners.**
@ -27,7 +27,7 @@ You can integrate the library into your own project, or simply use it in the com
### Command line:
```
hoymiles-wifi [-h] --host HOST [--as-json] <command>
hoymiles-wifi [-h] --host HOST [--local_addr IP_OF_INTERFACE_TO_USE] [--as-json] <command>
commands:
get-real-data-new,
@ -78,11 +78,11 @@ else:
- `async_set_power_limit(power_limit)`: Set the power limit of the inverter (0-100%)
- `async_set_wifi(wifi_ssid, wifi_password)`: Configure the wifi network
- `async_firmware_update()`: Update to latest firmware
- `async_restart_dtu`: Restart the DTU
- `async_turn_on_inverter`: Turn the inverter on
- `async_turn_off_inverter`: Turn the inverter off
- `async_get_information_data`: Retrieve information data
- `async_heartbeat`: Request a heartbeat message from the DTU
- `async_restart_dtu()`: Restart the DTU
- `async_turn_on_inverter()`: Turn the inverter on
- `async_turn_off_inverter()`: Turn the inverter off
- `async_get_information_data()`: Retrieve information data
- `async_heartbeat()`: Request a heartbeat message from the DTU
## Note

View File

@ -20,6 +20,7 @@ from hoymiles_wifi.hoymiles import (
generate_version_string,
get_dtu_model_name,
get_inverter_model_name,
get_meter_model_name,
)
from hoymiles_wifi.protobuf import (
AppGetHistPower_pb2,
@ -270,28 +271,44 @@ async def async_identify_dtu(dtu: DTU) -> str:
"""Identify the DTU asynchronously."""
real_data = await async_get_real_data_new(dtu)
return get_dtu_model_name(real_data.device_serial_number)
dtu_model_name = get_dtu_model_name(real_data.device_serial_number)
return {real_data.device_serial_number: dtu_model_name}
async def async_identify_inverters(dtu: DTU) -> list[str]:
"""Identify the DTU asynchronously."""
inverter_models = []
inverter_models = {}
real_data = await async_get_real_data_new(dtu)
if real_data:
for sgs_data in real_data.sgs_data:
serial_number = generate_inverter_serial_number(sgs_data.serial_number)
inverter_model = get_inverter_model_name(serial_number)
inverter_models.append(inverter_model)
inverter_models[serial_number] = inverter_model
for tgs_data in real_data.tgs_data:
serial_number = generate_inverter_serial_number(tgs_data.serial_number)
inverter_model = get_inverter_model_name(serial_number)
inverter_models.append(inverter_model)
inverter_models[serial_number] = inverter_model
return inverter_models
async def async_identify_meters(dtu: DTU) -> list[str]:
"""Identify the meters asynchronously."""
meter_models = {}
real_data = await async_get_real_data_new(dtu)
if real_data:
for meter_model in real_data.meter_data:
serial_number = generate_inverter_serial_number(meter_model.serial_number)
meter_model = get_meter_model_name(serial_number)
meter_models[serial_number] = meter_model
return meter_models
async def async_get_alarm_list(dtu: DTU) -> None:
"""Get alarm list from the dtu asynchronously."""
@ -312,6 +329,12 @@ async def main() -> None:
parser.add_argument(
"--host", type=str, required=True, help="IP address or hostname of the DTU"
)
parser.add_argument(
"--local_addr",
type=str,
required=False,
help="IP address of the interface to bind to",
)
parser.add_argument(
"--as-json",
action="store_true",
@ -339,13 +362,14 @@ async def main() -> None:
"heartbeat",
"identify-dtu",
"identify-inverters",
"identify-meters",
"get-alarm-list",
],
help="Command to execute",
)
args = parser.parse_args()
dtu = DTU(args.host)
dtu = DTU(args.host, args.local_addr)
# Execute the specified command using a switch case
switch = {
@ -366,6 +390,7 @@ async def main() -> None:
"heartbeat": async_heatbeat,
"identify-dtu": async_identify_dtu,
"identify-inverters": async_identify_inverters,
"identify-meters": async_identify_meters,
"get-alarm-list": async_get_alarm_list,
}
@ -376,15 +401,25 @@ async def main() -> None:
if args.as_json:
if isinstance(response, Message):
print(MessageToJson(response)) # noqa: T201
elif isinstance(response, dict):
print(json.dumps(response, indent=4)) # noqa: T201
else:
print(json.dumps(asdict(response), indent=4)) # noqa: T201
else:
print(f"{args.command.capitalize()} Response: \n{response}") # noqa: T201
else:
print( # noqa: T201
f"No response or unable to retrieve response for "
f"{args.command.replace('_', ' ')}",
)
if args.as_json:
print( # noqa: T201
json.dumps(
{"error": f"No response for {args.command.replace('_', ' ')}"},
indent=4,
)
)
else:
print( # noqa: T201
f"No response or unable to retrieve response for "
f"{args.command.replace('_', ' ')}",
)
sys.exit(2)

View File

@ -70,10 +70,11 @@ class NetworkState(Enum):
class DTU:
"""DTU class."""
def __init__(self, host: str):
def __init__(self, host: str, local_addr: str = None):
"""Initialize DTU class."""
self.host = host
self.local_addr = local_addr
self.state = NetworkState.Unknown
self.sequence = 0
self.mutex = asyncio.Lock()
@ -365,11 +366,13 @@ class DTU:
+ request_as_bytes
)
address = (self.host, dtu_port)
ip_to_bind = (self.local_addr, 0) if self.local_addr is not None else None
async with self.mutex:
try:
reader, writer = await asyncio.open_connection(*address)
reader, writer = await asyncio.open_connection(
host=self.host, port=dtu_port, local_addr=ip_to_bind
)
writer.write(message)
await writer.drain()

View File

@ -32,14 +32,11 @@ class InverterPower(Enum):
P_400 = "400"
P_500 = "500"
P_600_700_800 = "600/700/800"
P_800W = "800W"
P_1000 = "1000"
P_1000W = "1000W"
P_800W_1000W = "800W/1000W"
P_1000_1200_1500 = "1000/1200/1500"
P_1200_1500 = "1200/1500"
P_1600 = "1600"
P_2000 = "2000"
P_1600_2000 = "1600/2000"
P_2250 = "2250"
@ -56,7 +53,7 @@ power_mapping = {
0x1060: InverterPower.P_1000,
0x1061: InverterPower.P_1200_1500,
0x1161: InverterPower.P_1000_1200_1500,
0x1164: InverterPower.P_1600,
0x1164: InverterPower.P_1600_2000,
0x1412: InverterPower.P_800W_1000W,
0x1382: InverterPower.P_2250,
}
@ -127,7 +124,7 @@ class MeterType(Enum):
DTSU666 = "DTSU666"
meter_mapping = {0x1123: MeterType.DDSU666}
meter_mapping = {0x10C0: MeterType.DDSU666}
def format_number(number: int) -> str:

View File

@ -6,7 +6,7 @@ setup(
name="hoymiles-wifi",
packages=["hoymiles_wifi", "hoymiles_wifi.protobuf"],
install_requires=["protobuf", "crcmod"],
version="0.2.3",
version="0.2.4",
description="A python library for interfacing with the Hoymiles DTUs and the HMS-XXXXW-2T series of micro-inverters using protobuf messages.",
author="suaveolent",
include_package_data=True,