mirror of
https://github.com/suaveolent/hoymiles-wifi.git
synced 2024-09-19 19:00:17 +00:00
refactoring, version bump
This commit is contained in:
parent
a581179240
commit
a97c9a8fd2
@ -27,7 +27,7 @@
|
|||||||
],
|
],
|
||||||
"python.testing.unittestEnabled": false,
|
"python.testing.unittestEnabled": false,
|
||||||
"python.testing.pytestEnabled": true,
|
"python.testing.pytestEnabled": true,
|
||||||
"python.formatting.provider": "black",
|
"python.formatting.provider": "ruff",
|
||||||
"python.linting.mypyEnabled": true,
|
"python.linting.mypyEnabled": true,
|
||||||
"python.linting.enabled": true
|
"python.linting.enabled": true
|
||||||
}
|
}
|
||||||
|
29
README.md
29
README.md
@ -1,7 +1,6 @@
|
|||||||
# hoymiles-wifi
|
# hoymiles-wifi
|
||||||
|
|
||||||
|
This Python library facilitates communication with Hoymiles DTUs and the HMS-XXXXW-2T HMS microinverters, utilizing protobuf messages.
|
||||||
This Python library facilitates communication with Hoymiles HMS microinverters, specifically targeting the HMS-XXXXW-2T series.
|
|
||||||
|
|
||||||
**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.**
|
**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.**
|
||||||
|
|
||||||
@ -9,9 +8,8 @@ This Python library facilitates communication with Hoymiles HMS microinverters,
|
|||||||
|
|
||||||
The library was successfully tested with:
|
The library was successfully tested with:
|
||||||
|
|
||||||
- Hoymiles HMS-800W-2T
|
- Hoymiles HMS-800W-2T
|
||||||
- Hoymiles DTU WLite
|
- Hoymiles DTU WLite
|
||||||
|
|
||||||
|
|
||||||
## Installation
|
## Installation
|
||||||
|
|
||||||
@ -29,7 +27,7 @@ You can integrate the library into your own project, or simply use it in the com
|
|||||||
hoymiles-wifi [-h] --host HOST [--as-json] <command>
|
hoymiles-wifi [-h] --host HOST [--as-json] <command>
|
||||||
|
|
||||||
commands:
|
commands:
|
||||||
get-real-data-new,
|
get-real-data-new,
|
||||||
get-real-data-hms,
|
get-real-data-hms,
|
||||||
get-real-data,
|
get-real-data,
|
||||||
get-config,
|
get-config,
|
||||||
@ -52,18 +50,19 @@ The `--as-json` option is optional and allows formatting the output as JSON.
|
|||||||
### Python code
|
### Python code
|
||||||
|
|
||||||
```
|
```
|
||||||
from hoymiles_wifi.inverter import Inverter
|
from hoymiles_wifi.dtu import DTU
|
||||||
...
|
...
|
||||||
inverter = Inverter(<ip_address>)
|
dtu = DTU(<ip_address>)
|
||||||
response = await inverter.<command>
|
response = await dtu.<command>
|
||||||
|
|
||||||
if response:
|
if response:
|
||||||
print(f"Inverter Response: {response}")
|
print(f"DTU Response: {response}")
|
||||||
else:
|
else:
|
||||||
print("Unable to get response!")
|
print("Unable to get response!")
|
||||||
```
|
```
|
||||||
|
|
||||||
#### Available functions
|
#### Available functions
|
||||||
|
|
||||||
- `async_get_real_data_new()`: Retrieve real-time data
|
- `async_get_real_data_new()`: Retrieve real-time data
|
||||||
- `async_get_real_data_hms()`: Retrieve real-time data
|
- `async_get_real_data_hms()`: Retrieve real-time data
|
||||||
- `async_get_real_data()`: Retrieve real-time data
|
- `async_get_real_data()`: Retrieve real-time data
|
||||||
@ -82,15 +81,12 @@ else:
|
|||||||
|
|
||||||
Please be aware of the following considerations:
|
Please be aware of the following considerations:
|
||||||
|
|
||||||
- No DTU Implementation: This library
|
- No DTU Implementation: This library retrieves information directly from the (internal) DTU of Hoymiles Wifi inverters.
|
||||||
retrieves information directly from the internal DTU of Hoymiles Wifi
|
|
||||||
inverters.
|
|
||||||
|
|
||||||
## Caution
|
## Caution
|
||||||
|
|
||||||
Use this library responsibly and be aware of potential risks. There are no guarantees provided, and any misuse or incorrect implementation may result in undesirable outcomes. Ensure that your inverter is not compromised during communication.
|
Use this library responsibly and be aware of potential risks. There are no guarantees provided, and any misuse or incorrect implementation may result in undesirable outcomes. Ensure that your inverter is not compromised during communication.
|
||||||
|
|
||||||
|
|
||||||
## Known Limitations
|
## Known Limitations
|
||||||
|
|
||||||
**Update Frequency:** The library may experience limitations in fetching updates, potentially around twice per minute. The inverter firmware may enforce a mandatory wait period of approximately 30 seconds between requests.
|
**Update Frequency:** The library may experience limitations in fetching updates, potentially around twice per minute. The inverter firmware may enforce a mandatory wait period of approximately 30 seconds between requests.
|
||||||
@ -100,5 +96,6 @@ Use this library responsibly and be aware of potential risks. There are no guara
|
|||||||
## Attribution
|
## Attribution
|
||||||
|
|
||||||
A special thank you for the inspiration and codebase to:
|
A special thank you for the inspiration and codebase to:
|
||||||
- [DennisOSRM](https://github.com/DennisOSRM): [hms-mqtt-publisher](https://github.com/DennisOSRM/hms-mqtt-publisher)
|
|
||||||
- [henkwiedig](https://github.com/henkwiedig): [Hoymiles-DTU-Proto](https://github.com/henkwiedig/Hoymiles-DTU-Proto)
|
- [DennisOSRM](https://github.com/DennisOSRM): [hms-mqtt-publisher](https://github.com/DennisOSRM/hms-mqtt-publisher)
|
||||||
|
- [henkwiedig](https://github.com/henkwiedig): [Hoymiles-DTU-Proto](https://github.com/henkwiedig/Hoymiles-DTU-Proto)
|
||||||
|
@ -1,5 +1,7 @@
|
|||||||
|
"""Init file for hoymiles_wifi package."""
|
||||||
|
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
logging.basicConfig(level=logging.INFO)
|
logging.basicConfig(level=logging.INFO)
|
||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
@ -1,31 +1,51 @@
|
|||||||
|
"""Contains the main functionality of the hoymiles_wifi package."""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import asyncio
|
import asyncio
|
||||||
from dataclasses import dataclass, asdict
|
|
||||||
import json
|
import json
|
||||||
from google.protobuf.message import Message
|
from dataclasses import asdict, dataclass
|
||||||
|
|
||||||
from google.protobuf.json_format import MessageToJson
|
from google.protobuf.json_format import MessageToJson
|
||||||
from hoymiles_wifi.inverter import Inverter
|
from google.protobuf.message import Message
|
||||||
|
|
||||||
|
from hoymiles_wifi.const import DTU_FIRMWARE_URL_00_01_11, MAX_POWER_LIMIT
|
||||||
|
from hoymiles_wifi.dtu import DTU
|
||||||
from hoymiles_wifi.hoymiles import (
|
from hoymiles_wifi.hoymiles import (
|
||||||
generate_version_string,
|
|
||||||
generate_sw_version_string,
|
|
||||||
generate_dtu_version_string,
|
generate_dtu_version_string,
|
||||||
get_hw_model_name,
|
generate_sw_version_string,
|
||||||
get_dtu_model_name,
|
generate_version_string,
|
||||||
|
)
|
||||||
|
from hoymiles_wifi.protobuf import (
|
||||||
|
AppGetHistPower_pb2,
|
||||||
|
APPHeartbeatPB_pb2,
|
||||||
|
APPInfomationData_pb2,
|
||||||
|
CommandPB_pb2,
|
||||||
|
GetConfig_pb2,
|
||||||
|
InfomationData_pb2,
|
||||||
|
NetworkInfo_pb2,
|
||||||
|
RealData_pb2,
|
||||||
|
RealDataHMS_pb2,
|
||||||
|
RealDataNew_pb2,
|
||||||
)
|
)
|
||||||
|
|
||||||
from hoymiles_wifi.const import (
|
RED = "\033[91m"
|
||||||
DTU_FIRMWARE_URL_00_01_11
|
END = "\033[0m"
|
||||||
)
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class VersionInfo:
|
class VersionInfo:
|
||||||
|
"""Represents version information for the hoymiles_wifi package."""
|
||||||
|
|
||||||
dtu_hw_version: str
|
dtu_hw_version: str
|
||||||
dtu_sw_version: str
|
dtu_sw_version: str
|
||||||
inverter_hw_version: str
|
inverter_hw_version: str
|
||||||
inverter_sw_version: str
|
inverter_sw_version: str
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self: VersionInfo) -> str:
|
||||||
|
"""Return a string representation of the VersionInfo object."""
|
||||||
|
|
||||||
return (
|
return (
|
||||||
f'dtu_hw_version: "{self.dtu_hw_version}"\n'
|
f'dtu_hw_version: "{self.dtu_hw_version}"\n'
|
||||||
f'dtu_sw_version: "{self.dtu_sw_version}"\n'
|
f'dtu_sw_version: "{self.dtu_sw_version}"\n'
|
||||||
@ -33,91 +53,132 @@ class VersionInfo:
|
|||||||
f'inverter_sw_version: "{self.inverter_sw_version}"\n'
|
f'inverter_sw_version: "{self.inverter_sw_version}"\n'
|
||||||
)
|
)
|
||||||
|
|
||||||
def to_dict(self):
|
def to_dict(self: VersionInfo) -> dict:
|
||||||
|
"""Convert the VersionInfo object to a dictionary."""
|
||||||
|
|
||||||
return asdict(self)
|
return asdict(self)
|
||||||
|
|
||||||
|
|
||||||
# Inverter commands
|
# Inverter commands
|
||||||
async def async_get_real_data_new(inverter):
|
async def async_get_real_data_new(
|
||||||
return await inverter.async_get_real_data_new()
|
dtu: DTU,
|
||||||
|
) -> RealDataNew_pb2.RealDataNewResDTO | None:
|
||||||
|
"""Get real data from the inverter asynchronously."""
|
||||||
|
|
||||||
async def async_get_real_data_hms(inverter):
|
return await dtu.async_get_real_data_new()
|
||||||
return await inverter.async_get_real_data_hms()
|
|
||||||
|
|
||||||
async def async_get_real_data(inverter):
|
|
||||||
return await inverter.async_get_real_data()
|
|
||||||
|
|
||||||
async def async_get_config(inverter):
|
async def async_get_real_data_hms(
|
||||||
return await inverter.async_get_config()
|
dtu: DTU,
|
||||||
|
) -> RealDataHMS_pb2.RealDataHMSResDTO | None:
|
||||||
|
"""Get real data from the inverter asynchronously."""
|
||||||
|
|
||||||
async def async_network_info(inverter):
|
return await dtu.async_get_real_data_hms()
|
||||||
return await inverter.async_network_info()
|
|
||||||
|
|
||||||
async def async_app_information_data(inverter):
|
|
||||||
return await inverter.async_app_information_data()
|
|
||||||
|
|
||||||
async def async_app_get_hist_power(inverter):
|
async def async_get_real_data(dtu: DTU) -> RealData_pb2.RealDataResDTO | None:
|
||||||
return await inverter.async_app_get_hist_power()
|
"""Get real data from the inverter asynchronously."""
|
||||||
|
|
||||||
async def async_set_power_limit(inverter):
|
return await dtu.async_get_real_data()
|
||||||
|
|
||||||
RED = '\033[91m'
|
|
||||||
END = '\033[0m'
|
|
||||||
|
|
||||||
print(RED + "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! WARNING !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!" + END)
|
async def async_get_config(dtu: DTU) -> GetConfig_pb2.GetConfigResDTO | None:
|
||||||
print(RED + "!!! Danger zone! This will change the power limit of the inverter. !!!" + END)
|
"""Get the config from the inverter asynchronously."""
|
||||||
print(RED + "!!! Please be careful and make sure you know what you are doing. !!!" + END)
|
|
||||||
print(RED + "!!! Only proceed if you know what you are doing. !!!" + END)
|
|
||||||
print(RED + "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! WARNING !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!" + END)
|
|
||||||
print("")
|
|
||||||
|
|
||||||
|
return await dtu.async_get_config()
|
||||||
|
|
||||||
|
|
||||||
|
async def async_network_info(
|
||||||
|
dtu: DTU,
|
||||||
|
) -> NetworkInfo_pb2.NetworkInfoResDTO | None:
|
||||||
|
"""Get network information from the inverter asynchronously."""
|
||||||
|
|
||||||
|
return await dtu.async_network_info()
|
||||||
|
|
||||||
|
|
||||||
|
async def async_app_information_data(
|
||||||
|
dtu: DTU,
|
||||||
|
) -> APPInfomationData_pb2.AppInfomationDataResDTO | None:
|
||||||
|
"""Get application information data from the inverter asynchronously."""
|
||||||
|
|
||||||
|
return await dtu.async_app_information_data()
|
||||||
|
|
||||||
|
|
||||||
|
async def async_app_get_hist_power(
|
||||||
|
dtu: DTU,
|
||||||
|
) -> AppGetHistPower_pb2.AppGetHistPowerResDTO:
|
||||||
|
"""Get historical power data from the inverter asynchronously."""
|
||||||
|
|
||||||
|
return await dtu.async_app_get_hist_power()
|
||||||
|
|
||||||
|
|
||||||
|
async def async_set_power_limit(
|
||||||
|
dtu: DTU,
|
||||||
|
) -> CommandPB_pb2.CommandResDTO | None:
|
||||||
|
"""Set the power limit of the inverter asynchronously."""
|
||||||
|
|
||||||
|
print( # noqa: T201
|
||||||
|
RED
|
||||||
|
+ "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! WARNING !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n"
|
||||||
|
+ "!!! Danger zone! This will change the power limit of the dtu. !!!\n"
|
||||||
|
+ "!!! Please be careful and make sure you know what you are doing. !!!\n"
|
||||||
|
+ "!!! Only proceed if you know what you are doing. !!!\n"
|
||||||
|
+ "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! WARNING !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n"
|
||||||
|
+ END,
|
||||||
|
)
|
||||||
|
|
||||||
cont = input("Do you want to continue? (y/n): ")
|
cont = input("Do you want to continue? (y/n): ")
|
||||||
if(cont != 'y'):
|
if cont != "y":
|
||||||
return
|
return None
|
||||||
|
|
||||||
power_limit = int(input("Enter the new power limit (0-100): "))
|
power_limit = int(input("Enter the new power limit (0-100): "))
|
||||||
|
|
||||||
if(power_limit < 0 or power_limit > 100):
|
if power_limit < 0 or power_limit > MAX_POWER_LIMIT:
|
||||||
print("Error. Invalid power limit!")
|
print("Error. Invalid power limit!") # noqa: T201
|
||||||
return
|
return None
|
||||||
|
|
||||||
print(f"Setting power limit to {power_limit}%")
|
|
||||||
cont = input("Are you sure? (y/n): ")
|
|
||||||
|
|
||||||
if(cont != 'y'):
|
print(f"Setting power limit to {power_limit}%") # noqa: T201
|
||||||
return
|
cont = input("Are you sure? (y/n): ")
|
||||||
|
|
||||||
return await inverter.async_set_power_limit(power_limit)
|
if cont != "y":
|
||||||
|
return None
|
||||||
|
|
||||||
|
return await dtu.async_set_power_limit(power_limit)
|
||||||
|
|
||||||
|
|
||||||
async def async_set_wifi(inverter):
|
async def async_set_wifi(dtu: DTU) -> CommandPB_pb2.CommandResDTO | None:
|
||||||
|
"""Set the wifi SSID and password of the inverter asynchronously."""
|
||||||
|
|
||||||
wifi_ssid = input("Enter the new wifi SSID: ").strip()
|
wifi_ssid = input("Enter the new wifi SSID: ").strip()
|
||||||
wifi_password = input("Enter the new wifi password: ").strip()
|
wifi_password = input("Enter the new wifi password: ").strip()
|
||||||
print(f'Setting wifi to "{wifi_ssid}"')
|
print(f'Setting wifi to "{wifi_ssid}"') # noqa: T201
|
||||||
print(f'Setting wifi password to "{wifi_password}"')
|
print(f'Setting wifi password to "{wifi_password}"') # noqa: T201
|
||||||
cont = input("Are you sure? (y/n): ")
|
cont = input("Are you sure? (y/n): ")
|
||||||
if(cont != 'y'):
|
if cont != "y":
|
||||||
return
|
return None
|
||||||
return await inverter.async_set_wifi(wifi_ssid, wifi_password)
|
return await dtu.async_set_wifi(wifi_ssid, wifi_password)
|
||||||
|
|
||||||
async def async_firmware_update(inverter):
|
|
||||||
RED = '\033[91m'
|
|
||||||
END = '\033[0m'
|
|
||||||
|
|
||||||
print(RED + "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! WARNING !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!" + END)
|
async def async_firmware_update(dtu: DTU) -> CommandPB_pb2.CommandResDTO | None:
|
||||||
print(RED + "!!! Danger zone! This will update the firmeware of the DTU. !!!" + END)
|
"""Update the firmware of the DTU asynchronously."""
|
||||||
print(RED + "!!! Please be careful and make sure you know what you are doing. !!!" + END)
|
|
||||||
print(RED + "!!! Only proceed if you know what you are doing. !!!" + END)
|
print( # noqa: T201
|
||||||
print(RED + "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! WARNING !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!" + END)
|
RED
|
||||||
print("")
|
+ "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! WARNING !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n"
|
||||||
|
+ "!!! Danger zone! This will update the firmeware of the DTU. !!!\n"
|
||||||
|
+ "!!! Please be careful and make sure you know what you are doing. !!!\n"
|
||||||
|
+ "!!! Only proceed if you know what you are doing. !!!\n"
|
||||||
|
+ "!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! WARNING !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!\n"
|
||||||
|
+ END,
|
||||||
|
)
|
||||||
|
|
||||||
cont = input("Do you want to continue? (y/n): ")
|
cont = input("Do you want to continue? (y/n): ")
|
||||||
if(cont != 'y'):
|
if cont != "y":
|
||||||
return
|
return None
|
||||||
|
|
||||||
print("Please select a firmware version:")
|
print("Please select a firmware version:") # noqa: T201
|
||||||
print("1.) V00.01.11")
|
print("1.) V00.01.11") # noqa: T201
|
||||||
print("2.) Custom URL")
|
print("2.) Custom URL") # noqa: T201
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
selection = input("Enter your selection (1 or 2): ")
|
selection = input("Enter your selection (1 or 2): ")
|
||||||
@ -125,74 +186,106 @@ async def async_firmware_update(inverter):
|
|||||||
if selection == "1":
|
if selection == "1":
|
||||||
url = DTU_FIRMWARE_URL_00_01_11
|
url = DTU_FIRMWARE_URL_00_01_11
|
||||||
break
|
break
|
||||||
elif selection == "2":
|
if selection == "2":
|
||||||
url = input("Enter the custom URL: ").strip()
|
url = input("Enter the custom URL: ").strip()
|
||||||
break
|
break
|
||||||
else:
|
|
||||||
print("Invalid selection. Please enter 1 or 2.")
|
print("Invalid selection. Please enter 1 or 2.") # noqa: T201
|
||||||
|
|
||||||
print()
|
print() # noqa: T201
|
||||||
print(f'Firmware update URL: "{url}"')
|
print(f'Firmware update URL: "{url}"') # noqa: T201
|
||||||
print()
|
print() # noqa: T201
|
||||||
|
|
||||||
cont = input("Do you want to continue? (y/n): ")
|
cont = input("Do you want to continue? (y/n): ")
|
||||||
if(cont != 'y'):
|
if cont != "y":
|
||||||
return
|
return None
|
||||||
|
|
||||||
return await inverter.async_update_dtu_firmware()
|
|
||||||
|
|
||||||
async def async_restart(inverter):
|
return await dtu.async_update_dtu_firmware()
|
||||||
|
|
||||||
cont = input("Do you want to restart the device? (y/n): ")
|
|
||||||
if(cont != 'y'):
|
|
||||||
return
|
|
||||||
|
|
||||||
return await inverter.async_restart()
|
|
||||||
|
|
||||||
async def async_turn_off(inverter):
|
async def async_restart_dtu(dtu: DTU) -> CommandPB_pb2.CommandResDTO | None:
|
||||||
cont = input("Do you want to turn *OFF* the device? (y/n): ")
|
"""Restart the device asynchronously."""
|
||||||
if(cont != 'y'):
|
|
||||||
return
|
|
||||||
|
|
||||||
return await inverter.async_turn_off()
|
|
||||||
|
|
||||||
async def async_turn_on(inverter):
|
cont = input("Do you want to restart the DTU? (y/n): ")
|
||||||
cont = input("Do you want to turn *ON* the device? (y/n): ")
|
if cont != "y":
|
||||||
if(cont != 'y'):
|
return None
|
||||||
return
|
|
||||||
|
|
||||||
return await inverter.async_turn_on()
|
|
||||||
|
|
||||||
async def async_get_information_data(inverter):
|
return await dtu.async_restart_dtu()
|
||||||
return await inverter.async_get_information_data()
|
|
||||||
|
|
||||||
async def async_get_version_info(inverter):
|
|
||||||
response = await async_app_information_data(inverter)
|
async def async_turn_on_dtu(dtu: DTU) -> CommandPB_pb2.CommandResDTO | None:
|
||||||
|
"""Turn on the dtu asynchronously."""
|
||||||
|
|
||||||
|
cont = input("Do you want to turn *ON* the DTU? (y/n): ")
|
||||||
|
if cont != "y":
|
||||||
|
return None
|
||||||
|
|
||||||
|
return await dtu.async_turn_on_dtu()
|
||||||
|
|
||||||
|
|
||||||
|
async def async_turn_off_dtu(dtu: DTU) -> CommandPB_pb2.CommandResDTO | None:
|
||||||
|
"""Turn off the dtu asynchronously."""
|
||||||
|
|
||||||
|
cont = input("Do you want to turn *OFF* the DTU? (y/n): ")
|
||||||
|
if cont != "y":
|
||||||
|
return None
|
||||||
|
|
||||||
|
return await dtu.async_turn_off_dtu()
|
||||||
|
|
||||||
|
|
||||||
|
async def async_get_information_data(
|
||||||
|
dtu: DTU,
|
||||||
|
) -> InfomationData_pb2.InfomationDataResDTO:
|
||||||
|
"""Get information data from the dtu asynchronously."""
|
||||||
|
|
||||||
|
return await dtu.async_get_information_data()
|
||||||
|
|
||||||
|
|
||||||
|
async def async_get_version_info(dtu: DTU) -> VersionInfo | None:
|
||||||
|
"""Get version information from the dtu asynchronously."""
|
||||||
|
|
||||||
|
response = await async_app_information_data(dtu)
|
||||||
|
|
||||||
if not response:
|
if not response:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
return VersionInfo(
|
return VersionInfo(
|
||||||
dtu_hw_version="H" + generate_dtu_version_string(response.dtu_info.dtu_hw_version),
|
dtu_hw_version="H"
|
||||||
dtu_sw_version="V" + generate_dtu_version_string(response.dtu_info.dtu_sw_version),
|
+ generate_dtu_version_string(response.dtu_info.dtu_hw_version),
|
||||||
inverter_hw_version="H" + generate_version_string(response.pv_info[0].pv_hw_version),
|
dtu_sw_version="V"
|
||||||
inverter_sw_version="V" + generate_sw_version_string(response.pv_info[0].pv_sw_version),
|
+ generate_dtu_version_string(response.dtu_info.dtu_sw_version),
|
||||||
|
inverter_hw_version="H"
|
||||||
|
+ generate_version_string(response.pv_info[0].pv_hw_version),
|
||||||
|
inverter_sw_version="V"
|
||||||
|
+ generate_sw_version_string(response.pv_info[0].pv_sw_version),
|
||||||
)
|
)
|
||||||
|
|
||||||
async def async_heatbeat(inverter):
|
|
||||||
return await inverter.async_heartbeat()
|
|
||||||
|
|
||||||
def print_invalid_command(command):
|
async def async_heatbeat(dtu: DTU) -> APPHeartbeatPB_pb2.APPHeartbeatResDTO | None:
|
||||||
print(f"Invalid command: {command}")
|
"""Request a heartbeat from the dtu asynchronously."""
|
||||||
|
|
||||||
async def main():
|
return await dtu.async_heartbeat()
|
||||||
|
|
||||||
|
|
||||||
|
def print_invalid_command(command: str) -> None:
|
||||||
|
"""Print an invalid command message."""
|
||||||
|
|
||||||
|
print(f"Invalid command: {command}") # noqa: T201
|
||||||
|
|
||||||
|
|
||||||
|
async def main() -> None:
|
||||||
|
"""Execute the main function for the hoymiles_wifi package."""
|
||||||
|
|
||||||
parser = argparse.ArgumentParser(description="Hoymiles HMS Monitoring")
|
parser = argparse.ArgumentParser(description="Hoymiles HMS Monitoring")
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"--host", type=str, required=True, help="IP address or hostname of the inverter"
|
"--host", type=str, required=True, help="IP address or hostname of the DTU"
|
||||||
)
|
)
|
||||||
parser.add_argument('--as-json', action='store_true', default=False,
|
parser.add_argument(
|
||||||
help='Format the output as JSON')
|
"--as-json",
|
||||||
|
action="store_true",
|
||||||
|
default=False,
|
||||||
|
help="Format the output as JSON",
|
||||||
|
)
|
||||||
parser.add_argument(
|
parser.add_argument(
|
||||||
"command",
|
"command",
|
||||||
type=str,
|
type=str,
|
||||||
@ -217,46 +310,52 @@ async def main():
|
|||||||
help="Command to execute",
|
help="Command to execute",
|
||||||
)
|
)
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
inverter = Inverter(args.host)
|
dtu = DTU(args.host)
|
||||||
|
|
||||||
# Execute the specified command using a switch case
|
# Execute the specified command using a switch case
|
||||||
switch = {
|
switch = {
|
||||||
'get-real-data-new': async_get_real_data_new,
|
"get-real-data-new": async_get_real_data_new,
|
||||||
'get-real-data-hms': async_get_real_data_hms,
|
"get-real-data-hms": async_get_real_data_hms,
|
||||||
'get-real-data': async_get_real_data,
|
"get-real-data": async_get_real_data,
|
||||||
'get-config': async_get_config,
|
"get-config": async_get_config,
|
||||||
'network-info': async_network_info,
|
"network-info": async_network_info,
|
||||||
'app-information-data': async_app_information_data,
|
"app-information-data": async_app_information_data,
|
||||||
'app-get-hist-power': async_app_get_hist_power,
|
"app-get-hist-power": async_app_get_hist_power,
|
||||||
'set-power-limit': async_set_power_limit,
|
"set-power-limit": async_set_power_limit,
|
||||||
'set-wifi': async_set_wifi,
|
"set-wifi": async_set_wifi,
|
||||||
'firmware-update': async_firmware_update,
|
"firmware-update": async_firmware_update,
|
||||||
'restart': async_restart,
|
"restart": async_restart_dtu,
|
||||||
'turn-on': async_turn_on,
|
"turn-on": async_turn_on_dtu,
|
||||||
'turn-off': async_turn_off,
|
"turn-off": async_turn_off_dtu,
|
||||||
'get-information-data': async_get_information_data,
|
"get-information-data": async_get_information_data,
|
||||||
'get-version-info': async_get_version_info,
|
"get-version-info": async_get_version_info,
|
||||||
'heartbeat': async_heatbeat,
|
"heartbeat": async_heatbeat,
|
||||||
}
|
}
|
||||||
|
|
||||||
command_func = switch.get(args.command, print_invalid_command)
|
command_func = switch.get(args.command, print_invalid_command)
|
||||||
response = await command_func(inverter)
|
response = await command_func(dtu)
|
||||||
|
|
||||||
if response:
|
if response:
|
||||||
if args.as_json:
|
if args.as_json:
|
||||||
if isinstance(response, Message):
|
if isinstance(response, Message):
|
||||||
print(MessageToJson(response))
|
print(MessageToJson(response)) # noqa: T201
|
||||||
else:
|
else:
|
||||||
print(json.dumps(asdict(response), indent=4))
|
print(json.dumps(asdict(response), indent=4)) # noqa: T201
|
||||||
else:
|
else:
|
||||||
print(f"{args.command.capitalize()} Response: \n{response}")
|
print(f"{args.command.capitalize()} Response: \n{response}") # noqa: T201
|
||||||
else:
|
else:
|
||||||
print(f"No response or unable to retrieve response for {args.command.replace('_', ' ')}")
|
print( # noqa: T201
|
||||||
|
f"No response or unable to retrieve response for "
|
||||||
|
f"{args.command.replace('_', ' ')}",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def run_main():
|
def run_main() -> None:
|
||||||
|
"""Run the main function for the hoymiles_wifi package."""
|
||||||
|
|
||||||
asyncio.run(main())
|
asyncio.run(main())
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
run_main()
|
run_main()
|
||||||
|
@ -1,35 +1,37 @@
|
|||||||
INVERTER_PORT = 10081
|
"""Constants for the Hoymiles WiFi integration."""
|
||||||
|
|
||||||
|
DTU_PORT = 10081
|
||||||
|
|
||||||
# App -> DTU start with 0xa3, responses start 0xa2
|
# App -> DTU start with 0xa3, responses start 0xa2
|
||||||
CMD_HEADER = b'HM'
|
CMD_HEADER = b"HM"
|
||||||
CMD_APP_INFO_DATA_RES_DTO = b'\xa3\x01'
|
CMD_APP_INFO_DATA_RES_DTO = b"\xa3\x01"
|
||||||
CMD_HB_RES_DTO = b'\xa3\x02'
|
CMD_HB_RES_DTO = b"\xa3\x02"
|
||||||
CMD_REAL_DATA_RES_DTO = b'\xa3\x03'
|
CMD_REAL_DATA_RES_DTO = b"\xa3\x03"
|
||||||
CMD_W_INFO_RES_DTO = b'\xa3\x04'
|
CMD_W_INFO_RES_DTO = b"\xa3\x04"
|
||||||
CMD_COMMAND_RES_DTO = b'\xa3\x05'
|
CMD_COMMAND_RES_DTO = b"\xa3\x05"
|
||||||
CMD_COMMAND_STATUS_RES_DTO = b'\xa3\x06'
|
CMD_COMMAND_STATUS_RES_DTO = b"\xa3\x06"
|
||||||
CMD_DEV_CONFIG_FETCH_RES_DTO = b'\xa3\x07'
|
CMD_DEV_CONFIG_FETCH_RES_DTO = b"\xa3\x07"
|
||||||
CMD_DEV_CONFIG_PUT_RES_DTO = b'\xa3\x08'
|
CMD_DEV_CONFIG_PUT_RES_DTO = b"\xa3\x08"
|
||||||
CMD_GET_CONFIG = b'\xa3\x09'
|
CMD_GET_CONFIG = b"\xa3\x09"
|
||||||
CMD_SET_CONFIG = b'\xa3\x10'
|
CMD_SET_CONFIG = b"\xa3\x10"
|
||||||
CMD_REAL_RES_DTO = b'\xa3\x11'
|
CMD_REAL_RES_DTO = b"\xa3\x11"
|
||||||
CMD_GPST_RES_DTO = b'\xa3\x12'
|
CMD_GPST_RES_DTO = b"\xa3\x12"
|
||||||
CMD_AUTO_SEARCH = b'\xa3\x13'
|
CMD_AUTO_SEARCH = b"\xa3\x13"
|
||||||
CMD_NETWORK_INFO_RES = b'\xa3\x14'
|
CMD_NETWORK_INFO_RES = b"\xa3\x14"
|
||||||
CMD_APP_GET_HIST_POWER_RES = b'\xa3\x15'
|
CMD_APP_GET_HIST_POWER_RES = b"\xa3\x15"
|
||||||
CMD_APP_GET_HIST_ED_RES = b'\xa3\x16'
|
CMD_APP_GET_HIST_ED_RES = b"\xa3\x16"
|
||||||
CMD_HB_RES_DTO_ALT = b'\x83\x01'
|
CMD_HB_RES_DTO_ALT = b"\x83\x01"
|
||||||
CMD_REGISTER_RES_DTO = b'\x83\x02'
|
CMD_REGISTER_RES_DTO = b"\x83\x02"
|
||||||
CMD_STORAGE_DATA_RES = b'\x83\x03'
|
CMD_STORAGE_DATA_RES = b"\x83\x03"
|
||||||
CMD_COMMAND_RES_DTO_2 = b'\x83\x05'
|
CMD_COMMAND_RES_DTO_2 = b"\x83\x05"
|
||||||
CMD_COMMAND_STATUS_RES_DTO_2 = b'\x83\x06'
|
CMD_COMMAND_STATUS_RES_DTO_2 = b"\x83\x06"
|
||||||
CMD_DEV_CONFIG_FETCH_RES_DTO_2 = b'\x83\x07'
|
CMD_DEV_CONFIG_FETCH_RES_DTO_2 = b"\x83\x07"
|
||||||
CMD_DEV_CONFIG_PUT_RES_DTO_2 = b'\x83\x08'
|
CMD_DEV_CONFIG_PUT_RES_DTO_2 = b"\x83\x08"
|
||||||
CMD_GET_CONFIG_RES = b'\xdb\x08'
|
CMD_GET_CONFIG_RES = b"\xdb\x08"
|
||||||
CMD_SET_CONFIG_RES = b'\xdb\x07'
|
CMD_SET_CONFIG_RES = b"\xdb\x07"
|
||||||
|
|
||||||
CMD_CLOUD_INFO_DATA_RES_DTO = b'\x23\x01'
|
CMD_CLOUD_INFO_DATA_RES_DTO = b"\x23\x01"
|
||||||
CMD_CLOUD_COMMAND_RES_DTO = b'\x23\x05'
|
CMD_CLOUD_COMMAND_RES_DTO = b"\x23\x05"
|
||||||
|
|
||||||
CMD_ACTION_MICRO_DEFAULT = 0
|
CMD_ACTION_MICRO_DEFAULT = 0
|
||||||
CMD_ACTION_DTU_REBOOT = 1
|
CMD_ACTION_DTU_REBOOT = 1
|
||||||
@ -96,15 +98,19 @@ CMD_ACTION_INV_UPGRADE = 4112
|
|||||||
CMD_ACTION_BMS_UPGRADE = 4112
|
CMD_ACTION_BMS_UPGRADE = 4112
|
||||||
|
|
||||||
|
|
||||||
DEV_DTU = 1
|
DEV_DTU = 1
|
||||||
DEV_REPEATER = 2
|
DEV_REPEATER = 2
|
||||||
DEV_MICRO = 3
|
DEV_MICRO = 3
|
||||||
DEV_MODEL = 4
|
DEV_MODEL = 4
|
||||||
DEV_METER = 5
|
DEV_METER = 5
|
||||||
DEV_INV = 6
|
DEV_INV = 6
|
||||||
DEV_RSD = 7
|
DEV_RSD = 7
|
||||||
DEV_OP = 8
|
DEV_OP = 8
|
||||||
DEV_GATEWAY = 9
|
DEV_GATEWAY = 9
|
||||||
DEV_BMS = 10
|
DEV_BMS = 10
|
||||||
|
|
||||||
DTU_FIRMWARE_URL_00_01_11 = "http://fwupdate.hoymiles.com/cfs/bin/2311/06/,1488725943932555264.bin"
|
DTU_FIRMWARE_URL_00_01_11 = (
|
||||||
|
"http://fwupdate.hoymiles.com/cfs/bin/2311/06/,1488725943932555264.bin"
|
||||||
|
)
|
||||||
|
|
||||||
|
MAX_POWER_LIMIT = 100
|
||||||
|
@ -1,128 +1,185 @@
|
|||||||
|
"""DTU communication implementation for Hoymiles WiFi."""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
import struct
|
import struct
|
||||||
from typing import Any
|
|
||||||
from crcmod import mkCrcFun
|
|
||||||
from datetime import datetime
|
|
||||||
import time
|
import time
|
||||||
|
from datetime import datetime
|
||||||
|
from enum import Enum
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
|
from crcmod import mkCrcFun
|
||||||
|
|
||||||
from hoymiles_wifi import logger
|
from hoymiles_wifi import logger
|
||||||
|
from hoymiles_wifi.const import (
|
||||||
from hoymiles_wifi.utils import initialize_set_config
|
CMD_ACTION_DTU_REBOOT,
|
||||||
|
CMD_ACTION_DTU_UPGRADE,
|
||||||
|
CMD_ACTION_LIMIT_POWER,
|
||||||
|
CMD_ACTION_MI_SHUTDOWN,
|
||||||
|
CMD_ACTION_MI_START,
|
||||||
|
CMD_APP_GET_HIST_POWER_RES,
|
||||||
|
CMD_APP_INFO_DATA_RES_DTO,
|
||||||
|
CMD_CLOUD_COMMAND_RES_DTO,
|
||||||
|
CMD_COMMAND_RES_DTO,
|
||||||
|
CMD_GET_CONFIG,
|
||||||
|
CMD_HB_RES_DTO,
|
||||||
|
CMD_HEADER,
|
||||||
|
CMD_NETWORK_INFO_RES,
|
||||||
|
CMD_REAL_DATA_RES_DTO,
|
||||||
|
CMD_REAL_RES_DTO,
|
||||||
|
CMD_SET_CONFIG,
|
||||||
|
DEV_DTU,
|
||||||
|
DTU_FIRMWARE_URL_00_01_11,
|
||||||
|
DTU_PORT,
|
||||||
|
)
|
||||||
from hoymiles_wifi.protobuf import (
|
from hoymiles_wifi.protobuf import (
|
||||||
|
AppGetHistPower_pb2,
|
||||||
APPHeartbeatPB_pb2,
|
APPHeartbeatPB_pb2,
|
||||||
APPInfomationData_pb2,
|
APPInfomationData_pb2,
|
||||||
AppGetHistPower_pb2,
|
|
||||||
CommandPB_pb2,
|
CommandPB_pb2,
|
||||||
InfomationData_pb2,
|
|
||||||
GetConfig_pb2,
|
GetConfig_pb2,
|
||||||
RealData_pb2,
|
InfomationData_pb2,
|
||||||
RealDataNew_pb2,
|
|
||||||
RealDataHMS_pb2,
|
|
||||||
NetworkInfo_pb2,
|
NetworkInfo_pb2,
|
||||||
|
RealData_pb2,
|
||||||
|
RealDataHMS_pb2,
|
||||||
|
RealDataNew_pb2,
|
||||||
SetConfig_pb2,
|
SetConfig_pb2,
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
from hoymiles_wifi.const import (
|
|
||||||
INVERTER_PORT,
|
|
||||||
CMD_HEADER,
|
|
||||||
CMD_GET_CONFIG,
|
|
||||||
CMD_REAL_RES_DTO,
|
|
||||||
CMD_REAL_DATA_RES_DTO,
|
|
||||||
CMD_NETWORK_INFO_RES,
|
|
||||||
CMD_APP_INFO_DATA_RES_DTO,
|
|
||||||
CMD_APP_GET_HIST_POWER_RES,
|
|
||||||
CMD_ACTION_LIMIT_POWER,
|
|
||||||
CMD_COMMAND_RES_DTO,
|
|
||||||
CMD_ACTION_DTU_UPGRADE,
|
|
||||||
CMD_SET_CONFIG,
|
|
||||||
CMD_CLOUD_COMMAND_RES_DTO,
|
|
||||||
CMD_ACTION_DTU_REBOOT,
|
|
||||||
CMD_ACTION_MI_START,
|
|
||||||
CMD_ACTION_MI_SHUTDOWN,
|
|
||||||
DTU_FIRMWARE_URL_00_01_11,
|
|
||||||
CMD_HB_RES_DTO,
|
|
||||||
DEV_DTU,
|
|
||||||
)
|
)
|
||||||
|
from hoymiles_wifi.utils import initialize_set_config
|
||||||
|
|
||||||
|
|
||||||
class NetmodeSelect:
|
class NetmodeSelect(Enum):
|
||||||
|
"""Network mode selection."""
|
||||||
|
|
||||||
WIFI = 1
|
WIFI = 1
|
||||||
SIM = 2
|
SIM = 2
|
||||||
LAN = 3
|
LAN = 3
|
||||||
|
|
||||||
class NetworkState:
|
|
||||||
|
class NetworkState(Enum):
|
||||||
|
"""Network state."""
|
||||||
|
|
||||||
Unknown = 0
|
Unknown = 0
|
||||||
Online = 1
|
Online = 1
|
||||||
Offline = 2
|
Offline = 2
|
||||||
|
|
||||||
class Inverter:
|
|
||||||
|
class DTU:
|
||||||
|
"""DTU class."""
|
||||||
|
|
||||||
def __init__(self, host: str):
|
def __init__(self, host: str):
|
||||||
|
"""Initialize DTU class."""
|
||||||
|
|
||||||
self.host = host
|
self.host = host
|
||||||
self.state = NetworkState.Unknown
|
self.state = NetworkState.Unknown
|
||||||
self.sequence = 0
|
self.sequence = 0
|
||||||
self.mutex = asyncio.Lock()
|
self.mutex = asyncio.Lock()
|
||||||
|
|
||||||
def get_state(self) -> NetworkState:
|
def get_state(self) -> NetworkState:
|
||||||
|
"""Get DTU state."""
|
||||||
|
|
||||||
return self.state
|
return self.state
|
||||||
|
|
||||||
def set_state(self, new_state: NetworkState):
|
def set_state(self, new_state: NetworkState):
|
||||||
|
"""Set DTU state."""
|
||||||
|
|
||||||
if self.state != new_state:
|
if self.state != new_state:
|
||||||
self.state = new_state
|
self.state = new_state
|
||||||
logger.debug(f"Inverter is {new_state}")
|
logger.debug(f"DTU is {new_state}")
|
||||||
|
|
||||||
async def async_get_real_data_hms(self) -> RealDataHMS_pb2.HMSStateResponse | None:
|
async def async_get_real_data_hms(self) -> RealDataHMS_pb2.HMSStateResponse | None:
|
||||||
|
"""Get real data HMS."""
|
||||||
|
|
||||||
request = RealDataHMS_pb2.HMSRealDataResDTO()
|
request = RealDataHMS_pb2.HMSRealDataResDTO()
|
||||||
command = CMD_REAL_DATA_RES_DTO
|
command = CMD_REAL_DATA_RES_DTO
|
||||||
return await self.async_send_request(command, request, RealDataHMS_pb2.HMSStateResponse)
|
return await self.async_send_request(
|
||||||
|
command, request, RealDataHMS_pb2.HMSStateResponse
|
||||||
|
)
|
||||||
|
|
||||||
async def async_get_real_data(self) -> RealData_pb2.RealDataResDTO | None:
|
async def async_get_real_data(self) -> RealData_pb2.RealDataResDTO | None:
|
||||||
|
"""Get real data."""
|
||||||
|
|
||||||
request = RealData_pb2.RealDataResDTO()
|
request = RealData_pb2.RealDataResDTO()
|
||||||
command = CMD_REAL_DATA_RES_DTO
|
command = CMD_REAL_DATA_RES_DTO
|
||||||
return await self.async_send_request(command, request, RealData_pb2.RealDataReqDTO)
|
return await self.async_send_request(
|
||||||
|
command, request, RealData_pb2.RealDataReqDTO
|
||||||
|
)
|
||||||
|
|
||||||
async def async_get_real_data_new(self) -> RealDataNew_pb2.RealDataNewResDTO | None:
|
async def async_get_real_data_new(self) -> RealDataNew_pb2.RealDataNewResDTO | None:
|
||||||
|
"""Get real data new."""
|
||||||
|
|
||||||
request = RealDataNew_pb2.RealDataNewResDTO()
|
request = RealDataNew_pb2.RealDataNewResDTO()
|
||||||
request.time_ymd_hms = datetime.now().strftime("%Y-%m-%d %H:%M:%S").encode("utf-8")
|
request.time_ymd_hms = (
|
||||||
|
datetime.now().strftime("%Y-%m-%d %H:%M:%S").encode("utf-8")
|
||||||
|
)
|
||||||
request.offset = 28800
|
request.offset = 28800
|
||||||
request.time = int(time.time())
|
request.time = int(time.time())
|
||||||
command = CMD_REAL_RES_DTO
|
command = CMD_REAL_RES_DTO
|
||||||
return await self.async_send_request(command, request, RealDataNew_pb2.RealDataNewReqDTO)
|
return await self.async_send_request(
|
||||||
|
command, request, RealDataNew_pb2.RealDataNewReqDTO
|
||||||
|
)
|
||||||
|
|
||||||
async def async_get_config(self) -> GetConfig_pb2.GetConfigResDTO | None:
|
async def async_get_config(self) -> GetConfig_pb2.GetConfigResDTO | None:
|
||||||
|
"""Get config."""
|
||||||
|
|
||||||
request = GetConfig_pb2.GetConfigResDTO()
|
request = GetConfig_pb2.GetConfigResDTO()
|
||||||
request.offset = 28800
|
request.offset = 28800
|
||||||
request.time = int(time.time()) - 60
|
request.time = int(time.time()) - 60
|
||||||
command = CMD_GET_CONFIG
|
command = CMD_GET_CONFIG
|
||||||
return await self.async_send_request(command, request, GetConfig_pb2.GetConfigReqDTO)
|
return await self.async_send_request(
|
||||||
|
command,
|
||||||
|
request,
|
||||||
|
GetConfig_pb2.GetConfigReqDTO,
|
||||||
|
)
|
||||||
|
|
||||||
async def async_network_info(self) -> NetworkInfo_pb2.NetworkInfoResDTO | None:
|
async def async_network_info(self) -> NetworkInfo_pb2.NetworkInfoResDTO | None:
|
||||||
|
"""Get network info."""
|
||||||
|
|
||||||
request = NetworkInfo_pb2.NetworkInfoResDTO()
|
request = NetworkInfo_pb2.NetworkInfoResDTO()
|
||||||
request.offset = 28800
|
request.offset = 28800
|
||||||
request.time = int(time.time())
|
request.time = int(time.time())
|
||||||
command = CMD_NETWORK_INFO_RES
|
command = CMD_NETWORK_INFO_RES
|
||||||
return await self.async_send_request(command, request, NetworkInfo_pb2.NetworkInfoReqDTO)
|
return await self.async_send_request(
|
||||||
|
command, request, NetworkInfo_pb2.NetworkInfoReqDTO
|
||||||
async def async_app_information_data(self) -> APPInfomationData_pb2.APPInfoDataResDTO:
|
)
|
||||||
|
|
||||||
|
async def async_app_information_data(
|
||||||
|
self,
|
||||||
|
) -> APPInfomationData_pb2.APPInfoDataResDTO:
|
||||||
|
"""Get app information data."""
|
||||||
request = APPInfomationData_pb2.APPInfoDataResDTO()
|
request = APPInfomationData_pb2.APPInfoDataResDTO()
|
||||||
request.time_ymd_hms = datetime.now().strftime("%Y-%m-%d %H:%M:%S").encode("utf-8")
|
request.time_ymd_hms = (
|
||||||
|
datetime.now().strftime("%Y-%m-%d %H:%M:%S").encode("utf-8")
|
||||||
|
)
|
||||||
request.offset = 28800
|
request.offset = 28800
|
||||||
request.time = int(time.time())
|
request.time = int(time.time())
|
||||||
command = CMD_APP_INFO_DATA_RES_DTO
|
command = CMD_APP_INFO_DATA_RES_DTO
|
||||||
return await self.async_send_request(command, request, APPInfomationData_pb2.APPInfoDataReqDTO)
|
return await self.async_send_request(
|
||||||
|
command, request, APPInfomationData_pb2.APPInfoDataReqDTO
|
||||||
async def async_app_get_hist_power(self) -> AppGetHistPower_pb2.AppGetHistPowerResDTO | None:
|
)
|
||||||
|
|
||||||
|
async def async_app_get_hist_power(
|
||||||
|
self,
|
||||||
|
) -> AppGetHistPower_pb2.AppGetHistPowerResDTO | None:
|
||||||
|
"""Get historical power."""
|
||||||
|
|
||||||
request = AppGetHistPower_pb2.AppGetHistPowerResDTO()
|
request = AppGetHistPower_pb2.AppGetHistPowerResDTO()
|
||||||
request.offset = 28800
|
request.offset = 28800
|
||||||
request.requested_time = int(time.time())
|
request.requested_time = int(time.time())
|
||||||
command = CMD_APP_GET_HIST_POWER_RES
|
command = CMD_APP_GET_HIST_POWER_RES
|
||||||
return await self.async_send_request(command, request, AppGetHistPower_pb2.AppGetHistPowerReqDTO)
|
return await self.async_send_request(
|
||||||
|
command,
|
||||||
async def async_set_power_limit(self, power_limit: int) -> CommandPB_pb2.CommandResDTO | None:
|
request,
|
||||||
|
AppGetHistPower_pb2.AppGetHistPowerReqDTO,
|
||||||
|
)
|
||||||
|
|
||||||
if(power_limit < 0 or power_limit > 100):
|
async def async_set_power_limit(
|
||||||
|
self,
|
||||||
|
power_limit: int,
|
||||||
|
) -> CommandPB_pb2.CommandResDTO | None:
|
||||||
|
"""Set power limit."""
|
||||||
|
if power_limit < 0 or power_limit > 100:
|
||||||
logger.error("Error. Invalid power limit!")
|
logger.error("Error. Invalid power limit!")
|
||||||
return
|
return
|
||||||
|
|
||||||
@ -133,46 +190,58 @@ class Inverter:
|
|||||||
request.action = CMD_ACTION_LIMIT_POWER
|
request.action = CMD_ACTION_LIMIT_POWER
|
||||||
request.package_nub = 1
|
request.package_nub = 1
|
||||||
request.tid = int(time.time())
|
request.tid = int(time.time())
|
||||||
request.data = f'A:{power_limit},B:0,C:0\r'.encode('utf-8')
|
request.data = f"A:{power_limit},B:0,C:0\r".encode()
|
||||||
|
|
||||||
command = CMD_COMMAND_RES_DTO
|
command = CMD_COMMAND_RES_DTO
|
||||||
|
|
||||||
return await self.async_send_request(command, request, CommandPB_pb2.CommandReqDTO)
|
return await self.async_send_request(
|
||||||
|
command, request, CommandPB_pb2.CommandReqDTO
|
||||||
async def async_set_wifi(self, ssid: str, password: str) -> SetConfig_pb2.SetConfigResDTO | None:
|
)
|
||||||
|
|
||||||
|
async def async_set_wifi(
|
||||||
|
self, ssid: str, password: str
|
||||||
|
) -> SetConfig_pb2.SetConfigResDTO | None:
|
||||||
|
"""Set wifi."""
|
||||||
|
|
||||||
get_config_req = await self.async_get_config()
|
get_config_req = await self.async_get_config()
|
||||||
|
|
||||||
if(get_config_req is None):
|
if get_config_req is None:
|
||||||
logger.error("Failed to get config")
|
logger.error("Failed to get config")
|
||||||
return
|
return None
|
||||||
|
|
||||||
request = initialize_set_config(get_config_req)
|
request = initialize_set_config(get_config_req)
|
||||||
|
|
||||||
request.time = int(time.time())
|
request.time = int(time.time())
|
||||||
request.offset = 28800
|
request.offset = 28800
|
||||||
request.app_page = 1
|
request.app_page = 1
|
||||||
request.netmode_select = NetmodeSelect.WIFI
|
request.netmode_select = NetmodeSelect.WIFI
|
||||||
request.wifi_ssid = ssid.encode('utf-8')
|
request.wifi_ssid = ssid.encode("utf-8")
|
||||||
request.wifi_password = password.encode('utf-8')
|
request.wifi_password = password.encode("utf-8")
|
||||||
|
|
||||||
command = CMD_SET_CONFIG
|
command = CMD_SET_CONFIG
|
||||||
return await self.async_send_request(command, request, SetConfig_pb2.SetConfigReqDTO)
|
return await self.async_send_request(
|
||||||
|
command, request, SetConfig_pb2.SetConfigReqDTO
|
||||||
|
)
|
||||||
|
|
||||||
|
async def async_update_dtu_firmware(
|
||||||
async def async_update_dtu_firmware(self, firmware_url: str = DTU_FIRMWARE_URL_00_01_11) -> CommandPB_pb2.CommandResDTO | None:
|
self,
|
||||||
|
firmware_url: str = DTU_FIRMWARE_URL_00_01_11,
|
||||||
|
) -> CommandPB_pb2.CommandResDTO | None:
|
||||||
|
"""Update DTU firmware."""
|
||||||
|
|
||||||
request = CommandPB_pb2.CommandResDTO()
|
request = CommandPB_pb2.CommandResDTO()
|
||||||
request.action = CMD_ACTION_DTU_UPGRADE
|
request.action = CMD_ACTION_DTU_UPGRADE
|
||||||
request.package_nub = 1
|
request.package_nub = 1
|
||||||
request.tid = int(time.time())
|
request.tid = int(time.time())
|
||||||
request.data = (firmware_url + '\r').encode('utf-8')
|
request.data = (firmware_url + "\r").encode("utf-8")
|
||||||
|
|
||||||
command = CMD_CLOUD_COMMAND_RES_DTO
|
command = CMD_CLOUD_COMMAND_RES_DTO
|
||||||
return await self.async_send_request(command, request, CommandPB_pb2.CommandReqDTO)
|
return await self.async_send_request(
|
||||||
|
command, request, CommandPB_pb2.CommandReqDTO
|
||||||
|
)
|
||||||
|
|
||||||
async def async_restart(self) -> CommandPB_pb2.CommandResDTO | None:
|
async def async_restart_dtu(self) -> CommandPB_pb2.CommandResDTO | None:
|
||||||
|
"""Restart DTU."""
|
||||||
|
|
||||||
request = CommandPB_pb2.CommandResDTO()
|
request = CommandPB_pb2.CommandResDTO()
|
||||||
request.action = CMD_ACTION_DTU_REBOOT
|
request.action = CMD_ACTION_DTU_REBOOT
|
||||||
@ -180,10 +249,12 @@ class Inverter:
|
|||||||
request.tid = int(time.time())
|
request.tid = int(time.time())
|
||||||
|
|
||||||
command = CMD_CLOUD_COMMAND_RES_DTO
|
command = CMD_CLOUD_COMMAND_RES_DTO
|
||||||
return await self.async_send_request(command, request, CommandPB_pb2.CommandReqDTO)
|
return await self.async_send_request(
|
||||||
|
command, request, CommandPB_pb2.CommandReqDTO
|
||||||
|
)
|
||||||
|
|
||||||
|
async def async_turn_on_dtu(self) -> CommandPB_pb2.CommandResDTO | None:
|
||||||
async def async_turn_on(self) -> CommandPB_pb2.CommandResDTO | None:
|
"""Turn on DTU."""
|
||||||
|
|
||||||
request = CommandPB_pb2.CommandResDTO()
|
request = CommandPB_pb2.CommandResDTO()
|
||||||
request.action = CMD_ACTION_MI_START
|
request.action = CMD_ACTION_MI_START
|
||||||
@ -192,9 +263,12 @@ class Inverter:
|
|||||||
request.tid = int(time.time())
|
request.tid = int(time.time())
|
||||||
|
|
||||||
command = CMD_CLOUD_COMMAND_RES_DTO
|
command = CMD_CLOUD_COMMAND_RES_DTO
|
||||||
return await self.async_send_request(command, request, CommandPB_pb2.CommandReqDTO)
|
return await self.async_send_request(
|
||||||
|
command, request, CommandPB_pb2.CommandReqDTO
|
||||||
async def async_turn_off(self) -> CommandPB_pb2.CommandResDTO | None:
|
)
|
||||||
|
|
||||||
|
async def async_turn_off_dtu(self) -> CommandPB_pb2.CommandResDTO | None:
|
||||||
|
"""Turn off DTU."""
|
||||||
|
|
||||||
request = CommandPB_pb2.CommandResDTO()
|
request = CommandPB_pb2.CommandResDTO()
|
||||||
request.action = CMD_ACTION_MI_SHUTDOWN
|
request.action = CMD_ACTION_MI_SHUTDOWN
|
||||||
@ -203,41 +277,67 @@ class Inverter:
|
|||||||
request.tid = int(time.time())
|
request.tid = int(time.time())
|
||||||
|
|
||||||
command = CMD_CLOUD_COMMAND_RES_DTO
|
command = CMD_CLOUD_COMMAND_RES_DTO
|
||||||
return await self.async_send_request(command, request, CommandPB_pb2.CommandReqDTO)
|
return await self.async_send_request(
|
||||||
|
command, request, CommandPB_pb2.CommandReqDTO
|
||||||
async def async_get_information_data(self) -> InfomationData_pb2.InfoDataResDTO | None:
|
)
|
||||||
|
|
||||||
|
async def async_get_information_data(
|
||||||
|
self,
|
||||||
|
) -> InfomationData_pb2.InfoDataResDTO | None:
|
||||||
|
"""Get information data."""
|
||||||
|
|
||||||
request = InfomationData_pb2.InfoDataResDTO()
|
request = InfomationData_pb2.InfoDataResDTO()
|
||||||
request.time_ymd_hms = datetime.now().strftime("%Y-%m-%d %H:%M:%S").encode("utf-8")
|
request.time_ymd_hms = (
|
||||||
|
datetime.now().strftime("%Y-%m-%d %H:%M:%S").encode("utf-8")
|
||||||
|
)
|
||||||
request.offset = 28800
|
request.offset = 28800
|
||||||
request.time = int(time.time())
|
request.time = int(time.time())
|
||||||
command = CMD_APP_INFO_DATA_RES_DTO
|
command = CMD_APP_INFO_DATA_RES_DTO
|
||||||
return await self.async_send_request(command, request, InfomationData_pb2.InfoDataReqDTO)
|
return await self.async_send_request(
|
||||||
|
command, request, InfomationData_pb2.InfoDataReqDTO
|
||||||
|
)
|
||||||
|
|
||||||
async def async_heartbeat(self) -> APPHeartbeatPB_pb2.HBReqDTO | None:
|
async def async_heartbeat(self) -> APPHeartbeatPB_pb2.HBReqDTO | None:
|
||||||
|
"""Request heartbeat."""
|
||||||
|
|
||||||
request = APPHeartbeatPB_pb2.HBResDTO()
|
request = APPHeartbeatPB_pb2.HBResDTO()
|
||||||
request.time_ymd_hms = datetime.now().strftime("%Y-%m-%d %H:%M:%S").encode("utf-8")
|
request.time_ymd_hms = (
|
||||||
|
datetime.now().strftime("%Y-%m-%d %H:%M:%S").encode("utf-8")
|
||||||
|
)
|
||||||
request.offset = 28800
|
request.offset = 28800
|
||||||
request.time = int(time.time())
|
request.time = int(time.time())
|
||||||
|
|
||||||
command = CMD_HB_RES_DTO
|
command = CMD_HB_RES_DTO
|
||||||
return await self.async_send_request(command, request, APPHeartbeatPB_pb2.HBReqDTO)
|
return await self.async_send_request(
|
||||||
|
command, request, APPHeartbeatPB_pb2.HBReqDTO
|
||||||
|
)
|
||||||
|
|
||||||
|
async def async_send_request(
|
||||||
|
self,
|
||||||
|
command: bytes,
|
||||||
|
request: Any,
|
||||||
|
response_type: Any,
|
||||||
|
dtu_port: int = DTU_PORT,
|
||||||
|
):
|
||||||
|
"""Send request to DTU."""
|
||||||
|
|
||||||
async def async_send_request(self, command: bytes, request: Any, response_type: Any, inverter_port: int = INVERTER_PORT):
|
|
||||||
self.sequence = (self.sequence + 1) & 0xFFFF
|
self.sequence = (self.sequence + 1) & 0xFFFF
|
||||||
|
|
||||||
request_as_bytes = request.SerializeToString()
|
request_as_bytes = request.SerializeToString()
|
||||||
crc16 = mkCrcFun(0x18005, rev=True, initCrc=0xFFFF, xorOut=0x0000)(request_as_bytes)
|
crc16 = mkCrcFun(0x18005, rev=True, initCrc=0xFFFF, xorOut=0x0000)(
|
||||||
|
request_as_bytes
|
||||||
|
)
|
||||||
length = len(request_as_bytes) + 10
|
length = len(request_as_bytes) + 10
|
||||||
|
|
||||||
# compose request message
|
# compose request message
|
||||||
header = CMD_HEADER + command
|
header = CMD_HEADER + command
|
||||||
message = header + struct.pack('>HHH', self.sequence, crc16, length) + request_as_bytes
|
message = (
|
||||||
|
header
|
||||||
|
+ struct.pack(">HHH", self.sequence, crc16, length)
|
||||||
|
+ request_as_bytes
|
||||||
|
)
|
||||||
|
|
||||||
address = (self.host, inverter_port)
|
address = (self.host, dtu_port)
|
||||||
|
|
||||||
async with self.mutex:
|
async with self.mutex:
|
||||||
try:
|
try:
|
||||||
@ -261,18 +361,22 @@ class Inverter:
|
|||||||
try:
|
try:
|
||||||
if len(buf) < 10:
|
if len(buf) < 10:
|
||||||
raise ValueError("Buffer is too short for unpacking")
|
raise ValueError("Buffer is too short for unpacking")
|
||||||
|
|
||||||
crc16_target, read_length = struct.unpack('>HH', buf[6:10])
|
|
||||||
|
|
||||||
if(len(buf) != read_length):
|
crc16_target, read_length = struct.unpack(">HH", buf[6:10])
|
||||||
|
|
||||||
|
if len(buf) != read_length:
|
||||||
raise ValueError("Buffer is incomplete")
|
raise ValueError("Buffer is incomplete")
|
||||||
|
|
||||||
response_as_bytes = buf[10:read_length]
|
response_as_bytes = buf[10:read_length]
|
||||||
|
|
||||||
crc16_response = mkCrcFun(0x18005, rev=True, initCrc=0xFFFF, xorOut=0x0000)(response_as_bytes)
|
crc16_response = mkCrcFun(0x18005, rev=True, initCrc=0xFFFF, xorOut=0x0000)(
|
||||||
|
response_as_bytes
|
||||||
|
)
|
||||||
|
|
||||||
if crc16_response != crc16_target:
|
if crc16_response != crc16_target:
|
||||||
logger.debug(f"CRC16 mismatch: {hex(crc16_response)} != {hex(crc16_target)}")
|
logger.debug(
|
||||||
|
f"CRC16 mismatch: {hex(crc16_response)} != {hex(crc16_target)}"
|
||||||
|
)
|
||||||
raise ValueError("CRC16 mismatch")
|
raise ValueError("CRC16 mismatch")
|
||||||
|
|
||||||
parsed = response_type.FromString(response_as_bytes)
|
parsed = response_type.FromString(response_as_bytes)
|
@ -1,22 +1,31 @@
|
|||||||
""""Hoymiles quirks for inverters and DTU"""
|
"""Hoymiles quirks for inverters and DTU."""
|
||||||
|
|
||||||
from enum import Enum
|
|
||||||
import struct
|
import struct
|
||||||
|
from enum import Enum
|
||||||
|
|
||||||
from hoymiles_wifi import logger
|
from hoymiles_wifi import logger
|
||||||
|
|
||||||
|
|
||||||
class InverterType(Enum):
|
class InverterType(Enum):
|
||||||
|
"""Inverter type."""
|
||||||
|
|
||||||
ONE = "1T"
|
ONE = "1T"
|
||||||
TWO = "2T"
|
TWO = "2T"
|
||||||
FOUR = "4T"
|
FOUR = "4T"
|
||||||
SIX = "6T"
|
SIX = "6T"
|
||||||
|
|
||||||
|
|
||||||
class InverterSeries(Enum):
|
class InverterSeries(Enum):
|
||||||
|
"""Inverter series."""
|
||||||
|
|
||||||
HM = "HM"
|
HM = "HM"
|
||||||
HMS = "HMS"
|
HMS = "HMS"
|
||||||
HMT = "HMT"
|
HMT = "HMT"
|
||||||
|
|
||||||
|
|
||||||
class InverterPower(Enum):
|
class InverterPower(Enum):
|
||||||
|
"""Inverter power."""
|
||||||
|
|
||||||
P_100 = "100"
|
P_100 = "100"
|
||||||
P_250 = "250"
|
P_250 = "250"
|
||||||
P_300_350_400 = "300/350/400"
|
P_300_350_400 = "300/350/400"
|
||||||
@ -31,7 +40,27 @@ class InverterPower(Enum):
|
|||||||
P_2000 = "2000"
|
P_2000 = "2000"
|
||||||
|
|
||||||
|
|
||||||
|
power_mapping = {
|
||||||
|
0x1011: InverterPower.P_100,
|
||||||
|
0x1020: InverterPower.P_250,
|
||||||
|
0x1021: InverterPower.P_300_350_400,
|
||||||
|
0x1121: InverterPower.P_300_350_400,
|
||||||
|
0x1125: InverterPower.P_400,
|
||||||
|
0x1040: InverterPower.P_500,
|
||||||
|
0x1041: InverterPower.P_600_700_800,
|
||||||
|
0x1042: InverterPower.P_600_700_800,
|
||||||
|
0x1141: InverterPower.P_600_700_800,
|
||||||
|
0x1060: InverterPower.P_1000,
|
||||||
|
0x1061: InverterPower.P_1200_1500,
|
||||||
|
0x1161: InverterPower.P_1000_1200_1500,
|
||||||
|
0x1164: InverterPower.P_1600,
|
||||||
|
0x1412: InverterPower.P_800W,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class DTUType(Enum):
|
class DTUType(Enum):
|
||||||
|
"""DTU type."""
|
||||||
|
|
||||||
DTU_G100 = "DTU-G100"
|
DTU_G100 = "DTU-G100"
|
||||||
DTU_W100 = "DTU-W100"
|
DTU_W100 = "DTU-W100"
|
||||||
DTU_LITE_S = "DTU-Lite-S"
|
DTU_LITE_S = "DTU-Lite-S"
|
||||||
@ -43,43 +72,114 @@ class DTUType(Enum):
|
|||||||
DTU_W_LITE = "DTU-WLite"
|
DTU_W_LITE = "DTU-WLite"
|
||||||
|
|
||||||
|
|
||||||
def format_number(number) -> str:
|
type_mapping = {
|
||||||
return "{:02d}".format(number)
|
0x10F7: DTUType.DTU_PRO,
|
||||||
|
0x10FB: DTUType.DTU_PRO,
|
||||||
|
0x4101: DTUType.DTU_PRO,
|
||||||
|
0x10FC: DTUType.DTU_PRO,
|
||||||
|
0x4120: DTUType.DTU_PRO,
|
||||||
|
0x10F8: DTUType.DTU_PRO,
|
||||||
|
0x4100: DTUType.DTU_PRO,
|
||||||
|
0x10FD: DTUType.DTU_PRO,
|
||||||
|
0x4121: DTUType.DTU_PRO,
|
||||||
|
0x10D3: DTUType.DTU_W100_LITE_S,
|
||||||
|
0x4110: DTUType.DTU_W100_LITE_S,
|
||||||
|
0x10D8: DTUType.DTU_W100_LITE_S,
|
||||||
|
0x4130: DTUType.DTU_W100_LITE_S,
|
||||||
|
0x4132: DTUType.DTU_W100_LITE_S,
|
||||||
|
0x4133: DTUType.DTU_W100_LITE_S,
|
||||||
|
0x10D9: DTUType.DTU_W100_LITE_S,
|
||||||
|
0x4111: DTUType.DTU_W100_LITE_S,
|
||||||
|
0x10D2: DTUType.DTU_G100,
|
||||||
|
0x10D6: DTUType.DTU_LITE,
|
||||||
|
0x10D7: DTUType.DTU_LITE,
|
||||||
|
0x4131: DTUType.DTU_LITE,
|
||||||
|
0x1124: DTUType.DTU_HMS_W,
|
||||||
|
0x1125: DTUType.DTU_HMS_W,
|
||||||
|
0x1403: DTUType.DTU_HMS_W,
|
||||||
|
0x1144: DTUType.DTU_HMS_W,
|
||||||
|
0x1143: DTUType.DTU_HMS_W,
|
||||||
|
0x1145: DTUType.DTU_HMS_W,
|
||||||
|
0x1412: DTUType.DTU_HMS_W,
|
||||||
|
0x1164: DTUType.DTU_HMS_W,
|
||||||
|
0x1165: DTUType.DTU_HMS_W,
|
||||||
|
0x1166: DTUType.DTU_HMS_W,
|
||||||
|
0x1167: DTUType.DTU_HMS_W,
|
||||||
|
0x1222: DTUType.DTU_HMS_W,
|
||||||
|
0x1422: DTUType.DTU_HMS_W,
|
||||||
|
0x1423: DTUType.DTU_HMS_W,
|
||||||
|
0x1361: DTUType.DTU_HMS_W,
|
||||||
|
0x1362: DTUType.DTU_HMS_W,
|
||||||
|
0x1381: DTUType.DTU_HMS_W,
|
||||||
|
0x1382: DTUType.DTU_HMS_W,
|
||||||
|
0x4143: DTUType.DTU_HMS_W,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def format_number(number: int) -> str:
|
||||||
|
"""Format number to two digits."""
|
||||||
|
|
||||||
|
return f"{number:02d}"
|
||||||
|
|
||||||
|
|
||||||
def generate_version_string(version_number: int) -> str:
|
def generate_version_string(version_number: int) -> str:
|
||||||
version_string = format_number(version_number // 2048) + "." + format_number((version_number // 64) % 32) + "." + format_number(version_number % 64)
|
"""Generate version string."""
|
||||||
|
|
||||||
|
version_string = (
|
||||||
|
format_number(version_number // 2048)
|
||||||
|
+ "."
|
||||||
|
+ format_number((version_number // 64) % 32)
|
||||||
|
+ "."
|
||||||
|
+ format_number(version_number % 64)
|
||||||
|
)
|
||||||
return version_string
|
return version_string
|
||||||
|
|
||||||
|
|
||||||
def generate_sw_version_string(version_number: int) -> str:
|
def generate_sw_version_string(version_number: int) -> str:
|
||||||
|
"""Generate software version string."""
|
||||||
|
|
||||||
version_number2 = version_number // 10000
|
version_number2 = version_number // 10000
|
||||||
version_number3 = (version_number - (version_number2 * 10000)) // 100
|
version_number3 = (version_number - (version_number2 * 10000)) // 100
|
||||||
version_number4 = (version_number - (version_number2 * 10000)) - (version_number3 * 100)
|
version_number4 = (version_number - (version_number2 * 10000)) - (
|
||||||
|
version_number3 * 100
|
||||||
|
)
|
||||||
|
|
||||||
version_string = format_number(version_number2) + "." + format_number(version_number3) + "." + format_number(version_number4)
|
version_string = (
|
||||||
|
format_number(version_number2)
|
||||||
|
+ "."
|
||||||
|
+ format_number(version_number3)
|
||||||
|
+ "."
|
||||||
|
+ format_number(version_number4)
|
||||||
|
)
|
||||||
return version_string
|
return version_string
|
||||||
|
|
||||||
|
|
||||||
def generate_dtu_version_string(version_number: int, type: str = None) -> str:
|
def generate_dtu_version_string(version_number: int, type: str = "") -> str:
|
||||||
|
"""Generate DTU version string."""
|
||||||
|
|
||||||
version_string = ""
|
version_string = ""
|
||||||
version_number2 = version_number % 256
|
version_number2 = version_number % 256
|
||||||
version_number3 = (version_number // 256) % 16
|
version_number3 = (version_number // 256) % 16
|
||||||
|
|
||||||
if "SRF" == str:
|
if type == "SRF":
|
||||||
version_string += f"{format_number(version_number // 1048576)}.{format_number((version_number % 65536) // 4096)}.{format_number(version_number3)}.{format_number(version_number2)}"
|
version_string += f"{format_number(version_number // 1048576)}.{format_number((version_number % 65536) // 4096)}.{format_number(version_number3)}.{format_number(version_number2)}"
|
||||||
elif "HRF" == str:
|
elif type == "HRF":
|
||||||
version_string += f"{format_number(version_number // 65536)}.{format_number((version_number % 65536) // 4096)}.{format_number(version_number3)}.{format_number(version_number2)}"
|
version_string += f"{format_number(version_number // 65536)}.{format_number((version_number % 65536) // 4096)}.{format_number(version_number3)}.{format_number(version_number2)}"
|
||||||
else:
|
else:
|
||||||
version_string += f"{format_number(version_number // 4096)}.{format_number(version_number3)}.{format_number(version_number2)}"
|
version_string += f"{format_number(version_number // 4096)}.{format_number(version_number3)}.{format_number(version_number2)}"
|
||||||
|
|
||||||
return version_string
|
return version_string
|
||||||
|
|
||||||
|
|
||||||
def generate_inverter_serial_number(serial_number: int) -> str:
|
def generate_inverter_serial_number(serial_number: int) -> str:
|
||||||
|
"""Generate inverter serial number."""
|
||||||
|
|
||||||
return hex(serial_number)[2:]
|
return hex(serial_number)[2:]
|
||||||
|
|
||||||
|
|
||||||
def get_inverter_type(serial_bytes: bytes) -> InverterType:
|
def get_inverter_type(serial_bytes: bytes) -> InverterType:
|
||||||
|
"""Get inverter type."""
|
||||||
|
|
||||||
inverter_type = None
|
inverter_type = None
|
||||||
# Access individual bytes
|
# Access individual bytes
|
||||||
if serial_bytes[0] == 0x11:
|
if serial_bytes[0] == 0x11:
|
||||||
@ -95,17 +195,20 @@ def get_inverter_type(serial_bytes: bytes) -> InverterType:
|
|||||||
if serial_bytes[1] in [0x12]:
|
if serial_bytes[1] in [0x12]:
|
||||||
inverter_type = InverterType.TWO
|
inverter_type = InverterType.TWO
|
||||||
|
|
||||||
if inverter_type == None:
|
if inverter_type is None:
|
||||||
raise ValueError(f"Unknown inverter type: {hex(serial_bytes[0])} {hex(serial_bytes[1])}")
|
raise ValueError(
|
||||||
|
f"Unknown inverter type: {hex(serial_bytes[0])} {hex(serial_bytes[1])}"
|
||||||
|
)
|
||||||
|
|
||||||
return inverter_type
|
return inverter_type
|
||||||
|
|
||||||
|
|
||||||
def get_inverter_series(serial_bytes: bytes) -> InverterSeries:
|
def get_inverter_series(serial_bytes: bytes) -> InverterSeries:
|
||||||
|
"""Get inverter series."""
|
||||||
|
|
||||||
series = None
|
series = None
|
||||||
if serial_bytes[0] == 0x11:
|
if serial_bytes[0] == 0x11:
|
||||||
if (serial_bytes[1] & 0x0f) == 0x04:
|
if (serial_bytes[1] & 0x0F) == 0x04:
|
||||||
series = InverterSeries.HMS
|
series = InverterSeries.HMS
|
||||||
else:
|
else:
|
||||||
series = InverterSeries.HM
|
series = InverterSeries.HM
|
||||||
@ -118,52 +221,34 @@ def get_inverter_series(serial_bytes: bytes) -> InverterSeries:
|
|||||||
series = InverterSeries.HMT
|
series = InverterSeries.HMT
|
||||||
elif serial_bytes[0] == 0x14:
|
elif serial_bytes[0] == 0x14:
|
||||||
series = InverterSeries.HMS
|
series = InverterSeries.HMS
|
||||||
|
|
||||||
if series is None:
|
if series is None:
|
||||||
raise ValueError(f"Unknown series: {hex(serial_bytes[0])} {hex(serial_bytes[1])}!")
|
raise ValueError(
|
||||||
|
f"Unknown series: {hex(serial_bytes[0])} {hex(serial_bytes[1])}!"
|
||||||
|
)
|
||||||
|
|
||||||
return series
|
return series
|
||||||
|
|
||||||
|
|
||||||
def get_inverter_power(serial_bytes: bytes) -> InverterPower:
|
def get_inverter_power(serial_bytes: bytes) -> InverterPower:
|
||||||
|
"""Get inverter power."""
|
||||||
inverter_type_bytes = struct.unpack('>H', serial_bytes[:2])[0]
|
|
||||||
|
|
||||||
power = None
|
inverter_type_bytes = struct.unpack(">H", serial_bytes[:2])[0]
|
||||||
|
|
||||||
if inverter_type_bytes in [0x1011]:
|
power = power_mapping.get(inverter_type_bytes)
|
||||||
power = InverterPower.P_100
|
|
||||||
elif inverter_type_bytes in [0x1020]:
|
|
||||||
power = InverterPower.P_250
|
|
||||||
elif inverter_type_bytes in [0x1021, 0x1121]:
|
|
||||||
power = InverterPower.P_300_350_400
|
|
||||||
elif inverter_type_bytes in [0x1125]:
|
|
||||||
power = InverterPower.P_400
|
|
||||||
elif inverter_type_bytes in [0x1040]:
|
|
||||||
power = InverterPower.P_500
|
|
||||||
elif inverter_type_bytes in [0x1041, 0x1042, 0x1141]:
|
|
||||||
power = InverterPower.P_600_700_800
|
|
||||||
elif inverter_type_bytes in [0x1060]:
|
|
||||||
power = InverterPower.P_1000
|
|
||||||
elif inverter_type_bytes in [0x1061]:
|
|
||||||
power = InverterPower.P_1200_1500
|
|
||||||
elif inverter_type_bytes in [0x1161]:
|
|
||||||
power = InverterPower.P_1000_1200_1500
|
|
||||||
elif inverter_type_bytes in [0x1164]:
|
|
||||||
power = InverterPower.P_1600
|
|
||||||
elif inverter_type_bytes in [0x1412]:
|
|
||||||
power = InverterPower.P_800W
|
|
||||||
|
|
||||||
if power is None:
|
if power is None:
|
||||||
raise ValueError(f"Unknown power: {hex(serial_bytes[0])} {hex(serial_bytes[1])}!")
|
raise ValueError(
|
||||||
|
f"Unknown power: {hex(serial_bytes[0])} {hex(serial_bytes[1])}!"
|
||||||
|
)
|
||||||
|
|
||||||
return power
|
return power
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
def get_hw_model_name(serial_number: str) -> str:
|
def get_hw_model_name(serial_number: str) -> str:
|
||||||
|
"""Get hardware model name."""
|
||||||
|
|
||||||
if(serial_number == "22069994886948"):
|
if serial_number == "22069994886948":
|
||||||
serial_number = generate_inverter_serial_number(int(serial_number))
|
serial_number = generate_inverter_serial_number(int(serial_number))
|
||||||
|
|
||||||
serial_bytes = bytes.fromhex(serial_number)
|
serial_bytes = bytes.fromhex(serial_number)
|
||||||
@ -176,40 +261,32 @@ def get_hw_model_name(serial_number: str) -> str:
|
|||||||
logger.error(e)
|
logger.error(e)
|
||||||
return "Unknown"
|
return "Unknown"
|
||||||
else:
|
else:
|
||||||
inverter_model_name = inverter_series.value + "-" + inverter_power.value + "-" + inverter_type.value
|
inverter_model_name = (
|
||||||
|
inverter_series.value
|
||||||
|
+ "-"
|
||||||
|
+ inverter_power.value
|
||||||
|
+ "-"
|
||||||
|
+ inverter_type.value
|
||||||
|
)
|
||||||
return inverter_model_name
|
return inverter_model_name
|
||||||
|
|
||||||
|
|
||||||
def get_dtu_model_type(serial_bytes: bytes) -> DTUType:
|
def get_dtu_model_type(serial_bytes: bytes) -> DTUType:
|
||||||
|
"""Get DTU model type."""
|
||||||
|
|
||||||
dtu_type = None
|
dtu_type_bytes = struct.unpack(">H", serial_bytes[:2])[0]
|
||||||
|
|
||||||
dtu_type_bytes = struct.unpack('>H', serial_bytes[:2])[0]
|
dtu_type = type_mapping.get(dtu_type_bytes)
|
||||||
|
|
||||||
if (dtu_type_bytes in [0x10F7] or
|
|
||||||
dtu_type_bytes in [0x10FB, 0x4101, 0x10FC, 0x4120] or
|
|
||||||
dtu_type_bytes in [0x10F8, 0x4100, 0x10FD, 0x4121]):
|
|
||||||
dtu_type = DTUType.DTU_PRO
|
|
||||||
elif dtu_type_bytes in [0x10D3, 0x4110, 0x10D8, 0x4130, 0x4132, 0x4133, 0x10D9, 0x4111]:
|
|
||||||
dtu_type = DTUType.DTU_W100_LITE_S
|
|
||||||
elif dtu_type_bytes in [0x10D2]:
|
|
||||||
dtu_type = DTUType.DTU_G100
|
|
||||||
elif dtu_type_bytes in [0x10D6, 0x10D7, 0x4131]:
|
|
||||||
dtu_type = DTUType.DTU_LITE
|
|
||||||
elif (dtu_type_bytes in [0x1124, 0x1125, 0x1403] or
|
|
||||||
dtu_type_bytes in [0x1144, 0x1143, 0x1145, 0x1412] or
|
|
||||||
dtu_type_bytes in [0x1164, 0x1165, 0x1166, 0x1167, 0x1222, 0x1422, 0x1423] or
|
|
||||||
dtu_type_bytes in [0x1361, 0x1362] or
|
|
||||||
dtu_type_bytes in [0x1381, 0x1382] or
|
|
||||||
dtu_type_bytes in [0x4143]):
|
|
||||||
dtu_type = DTUType.DTU_HMS_W
|
|
||||||
|
|
||||||
if dtu_type is None:
|
if dtu_type is None:
|
||||||
raise ValueError(f"Unknown DTU: {serial_bytes[:2]}!")
|
raise ValueError(f"Unknown DTU: {serial_bytes[:2]}!")
|
||||||
|
|
||||||
return dtu_type
|
return dtu_type
|
||||||
|
|
||||||
|
|
||||||
def get_dtu_model_name(serial_number: str) -> str:
|
def get_dtu_model_name(serial_number: str) -> str:
|
||||||
|
"""Get DTU model name."""
|
||||||
|
|
||||||
serial_bytes = bytes.fromhex(serial_number)
|
serial_bytes = bytes.fromhex(serial_number)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@ -219,4 +296,3 @@ def get_dtu_model_name(serial_number: str) -> str:
|
|||||||
return "Unknown"
|
return "Unknown"
|
||||||
else:
|
else:
|
||||||
return dtu_type.value
|
return dtu_type.value
|
||||||
|
|
||||||
|
@ -2,5 +2,6 @@
|
|||||||
|
|
||||||
for file in $(ls *.proto)
|
for file in $(ls *.proto)
|
||||||
do
|
do
|
||||||
|
protoc --pyi_out=. $file
|
||||||
protoc --python_out=. $file
|
protoc --python_out=. $file
|
||||||
done
|
done
|
||||||
|
@ -1,11 +1,14 @@
|
|||||||
""""Utils for interacting with Hoymiles WiFi API."""
|
"""Utils for interacting with Hoymiles WiFi API."""
|
||||||
|
|
||||||
from hoymiles_wifi.protobuf import (
|
from hoymiles_wifi.protobuf import (
|
||||||
GetConfig_pb2,
|
GetConfig_pb2,
|
||||||
SetConfig_pb2,
|
SetConfig_pb2,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def initialize_set_config(get_config_req: GetConfig_pb2.GetConfigReqDTO):
|
def initialize_set_config(get_config_req: GetConfig_pb2.GetConfigReqDTO):
|
||||||
|
"""Initialize set config response with get config request."""
|
||||||
|
|
||||||
set_config_res = SetConfig_pb2.SetConfigResDTO()
|
set_config_res = SetConfig_pb2.SetConfigResDTO()
|
||||||
set_config_res.lock_password = get_config_req.lock_password
|
set_config_res.lock_password = get_config_req.lock_password
|
||||||
set_config_res.lock_time = get_config_req.lock_time
|
set_config_res.lock_time = get_config_req.lock_time
|
||||||
@ -27,8 +30,8 @@ def initialize_set_config(get_config_req: GetConfig_pb2.GetConfigReqDTO):
|
|||||||
set_config_res.access_model = get_config_req.access_model
|
set_config_res.access_model = get_config_req.access_model
|
||||||
set_config_res.mac_0 = get_config_req.mac_0
|
set_config_res.mac_0 = get_config_req.mac_0
|
||||||
set_config_res.mac_1 = get_config_req.mac_1
|
set_config_res.mac_1 = get_config_req.mac_1
|
||||||
set_config_res.mac_2 = get_config_req.mac_2
|
set_config_res.mac_2 = get_config_req.mac_2
|
||||||
set_config_res.mac_3 = get_config_req.mac_3
|
set_config_res.mac_3 = get_config_req.mac_3
|
||||||
set_config_res.mac_4 = get_config_req.mac_4
|
set_config_res.mac_4 = get_config_req.mac_4
|
||||||
set_config_res.mac_5 = get_config_req.mac_5
|
set_config_res.mac_5 = get_config_req.mac_5
|
||||||
set_config_res.dhcp_switch = get_config_req.dhcp_switch
|
set_config_res.dhcp_switch = get_config_req.dhcp_switch
|
||||||
@ -56,7 +59,3 @@ def initialize_set_config(get_config_req: GetConfig_pb2.GetConfigReqDTO):
|
|||||||
set_config_res.dtu_ap_pass = get_config_req.dtu_ap_pass
|
set_config_res.dtu_ap_pass = get_config_req.dtu_ap_pass
|
||||||
|
|
||||||
return set_config_res
|
return set_config_res
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
25
setup.py
25
setup.py
@ -1,19 +1,18 @@
|
|||||||
|
"""Setup for the hoymiles-wifi package."""
|
||||||
|
|
||||||
from setuptools import setup
|
from setuptools import setup
|
||||||
|
|
||||||
setup(
|
setup(
|
||||||
name='hoymiles-wifi',
|
name="hoymiles-wifi",
|
||||||
packages=['hoymiles_wifi', 'hoymiles_wifi.protobuf'],
|
packages=["hoymiles_wifi", "hoymiles_wifi.protobuf"],
|
||||||
install_requires=[
|
install_requires=["protobuf", "crcmod"],
|
||||||
'protobuf',
|
version="0.1.8",
|
||||||
'crcmod'
|
description="A python library for interfacing with the Hoymiles DTUs and the HMS-XXXXW-2T series of micro-inverters using protobuf messages.",
|
||||||
],
|
author="suaveolent",
|
||||||
version='0.1.7',
|
|
||||||
description='A python library for interfacing with Hoymiles HMS-XXXXW-2T series of micro-inverters.',
|
|
||||||
author='suaveolent',
|
|
||||||
include_package_data=True,
|
include_package_data=True,
|
||||||
entry_points={
|
entry_points={
|
||||||
'console_scripts': [
|
"console_scripts": [
|
||||||
'hoymiles-wifi = hoymiles_wifi.__main__:run_main',
|
"hoymiles-wifi = hoymiles_wifi.__main__:run_main",
|
||||||
],
|
],
|
||||||
}
|
},
|
||||||
)
|
)
|
||||||
|
Loading…
Reference in New Issue
Block a user