-
-
Save smashnet/82ad0b9d7f0ba2e5098e6649ba08f88a to your computer and use it in GitHub Desktop.
#!/usr/bin/env python3 | |
# -*- coding: utf-8 -*- | |
""" | |
Library to get a lot of useful data out of Senec appliances. | |
Tested with: SENEC.Home V3 hybrid duo | |
Kudos: | |
* SYSTEM_STATE_NAME taken from https://github.com/mchwalisz/pysenec | |
""" | |
import requests | |
import struct | |
import logging | |
import json | |
import urllib3 | |
urllib3.disable_warnings(urllib3.exceptions.InsecureRequestWarning) | |
__author__ = "Nicolas Inden" | |
__copyright__ = "Copyright 2023, Nicolas Inden" | |
__credits__ = ["Nicolas Inden", "Mikołaj Chwalisz"] | |
__license__ = "Apache-2.0 License" | |
__version__ = "1.0.4" | |
__maintainer__ = "Nicolas Inden" | |
__email__ = "[email protected]" | |
__status__ = "Production" | |
logging.basicConfig(format='%(asctime)s %(levelname)s:%(message)s',level=logging.INFO) | |
log = logging.getLogger("Senec") | |
class Senec(): | |
def __init__(self, device_ip): | |
self.device_ip = device_ip | |
self.read_api = f"https://{device_ip}/lala.cgi" | |
def get_values(self, request_json = {}): | |
if not request_json: request_json = BASIC_REQUEST | |
try: | |
response = requests.post(self.read_api, json=request_json, verify=False) | |
if response.status_code == 200: | |
return self.__decode_data(response.json()) | |
#return self.__substitute_system_state(res) | |
else: | |
log.warning(f"Status code {response.status_code}") | |
return {"error": f"Status code {response.status_code}"} | |
except requests.Timeout: | |
errmsg = f"{self.device_ip}: Timeout while accessing Senec box." | |
log.warning(errmsg) | |
return {"error": errmsg} | |
except requests.ConnectionError: | |
errmsg = f"{self.device_ip}: Connection error while accessing Senec box." | |
log.warning(errmsg) | |
return {"error": errmsg} | |
except requests.exceptions.JSONDecodeError as e: | |
errmsg = f"{self.device_ip}: Could not decode JSON response: {e}" | |
log.warning(errmsg) | |
return {"error": errmsg} | |
except Exception as e: | |
errmsg = f"{self.device_ip}: Other exception: {e}" | |
log.warning(errmsg) | |
return {"error": errmsg} | |
def get_all_values(self): | |
request_json = {"STATISTIC": {},"ENERGY": {},"FEATURES": {},"LOG": {},"SYS_UPDATE": {},"WIZARD": {},"BMS": {},"BAT1": {},"BAT1OBJ1": {},"BAT1OBJ2": {},"BAT1OBJ3": {},"BAT1OBJ4": {},"PWR_UNIT": {},"PV1": {},"FACTORY": {},"GRIDCONFIG": {}} | |
return self.get_values(request_json) | |
def __decode_data(self, data): | |
return { k: self.__decode_data_helper(v) for k, v in data.items() } | |
def __decode_data_helper(self, data): | |
if isinstance(data, str): | |
return self.__decode_value(data) | |
if isinstance(data, list): | |
return [self.__decode_value(val) for val in data] | |
if isinstance(data, dict): | |
return { k: self.__decode_data_helper(v) for k, v in data.items() } | |
def __decode_value(self, value): | |
if value.startswith("fl_"): | |
return struct.unpack('!f', bytes.fromhex(value[3:]))[0] | |
if value.startswith("u8_"): | |
return struct.unpack('!B', bytes.fromhex(value[3:]))[0] | |
if value.startswith("i3_") or value.startswith("i8_") or value.startswith("u3_") or value.startswith("u1_"): | |
return int(value[3:], 16) | |
if value.startswith("st_"): | |
return value[3:] | |
return value | |
def __substitute_system_state(self, data): | |
#currently unused | |
system_state = data['STATISTIC']['CURRENT_STATE'] | |
if system_state == "VARIABLE_NOT_FOUND": | |
return data | |
try: | |
data['STATISTIC']['CURRENT_STATE'] = SYSTEM_STATE_NAME[system_state] | |
except KeyError as e: | |
log.error(f"Failed substituting system state: {e}") | |
data['STATISTIC']['CURRENT_STATE'] = f"unknown ({system_state})" | |
finally: | |
return data | |
BASIC_REQUEST = { | |
#'STATISTIC': { | |
# 'CURRENT_STATE': '', # Current state of the system (int, see SYSTEM_STATE_NAME) | |
# 'LIVE_BAT_CHARGE_MASTER': '', # Battery charge amount since installation (kWh) | |
# 'LIVE_BAT_DISCHARGE_MASTER': '', # Battery discharge amount since installation (kWh) | |
# 'LIVE_GRID_EXPORT': '', # Grid export amount since installation (kWh) | |
# 'LIVE_GRID_IMPORT': '', # Grid import amount since installation (kWh) | |
# 'LIVE_HOUSE_CONS': '', # House consumption since installation (kWh) | |
# 'LIVE_PV_GEN': '', # PV generated power since installation (kWh) | |
# 'MEASURE_TIME': '' # Unix timestamp for above values (ms) | |
#}, | |
'ENERGY': { | |
'GUI_BAT_DATA_CURRENT': '', # Battery charge current: negative if discharging, positiv if charging (A) | |
'GUI_BAT_DATA_FUEL_CHARGE': '', # Remaining battery (percent) | |
'GUI_BAT_DATA_POWER': '', # Battery charge power: negative if discharging, positiv if charging (W) | |
'GUI_BAT_DATA_VOLTAGE': '', # Battery voltage (V) | |
'GUI_GRID_POW': '', # Grid power: negative if exporting, positiv if importing (W) | |
'GUI_HOUSE_POW': '', # House power consumption (W) | |
'GUI_INVERTER_POWER': '', # PV production (W) | |
'STAT_HOURS_OF_OPERATION': '' # Appliance hours of operation | |
}, | |
'BMS': { | |
'CHARGED_ENERGY': '', # List: Charged energy per battery | |
'DISCHARGED_ENERGY': '', # List: Discharged energy per battery | |
'CYCLES': '' # List: Cycles per battery | |
}, | |
'PV1': { | |
'MPP_CUR': '', # List: MPP current (A) | |
'MPP_POWER': '', # List: MPP power (W) | |
'MPP_VOL': '', # List: MPP voltage (V) | |
'POWER_RATIO': '', # Grid export limit (percent) | |
'P_TOTAL': '' # ? | |
}, | |
'FACTORY': { | |
'DESIGN_CAPACITY': '', # Battery design capacity (Wh) | |
'MAX_CHARGE_POWER_DC': '', # Battery max charging power (W) | |
'MAX_DISCHARGE_POWER_DC': '' # Battery max discharging power (W) | |
} | |
} | |
SYSTEM_STATE_NAME = { | |
0: "INITIAL STATE", | |
1: "ERROR INVERTER COMMUNICATION", | |
2: "ERROR ELECTRICY METER", | |
3: "RIPPLE CONTROL RECEIVER", | |
4: "INITIAL CHARGE", | |
5: "MAINTENANCE CHARGE", | |
6: "MAINTENANCE READY", | |
7: "MAINTENANCE REQUIRED", | |
8: "MAN. SAFETY CHARGE", | |
9: "SAFETY CHARGE READY", | |
10: "FULL CHARGE", | |
11: "EQUALIZATION: CHARGE", | |
12: "DESULFATATION: CHARGE", | |
13: "BATTERY FULL", | |
14: "CHARGE", | |
15: "BATTERY EMPTY", | |
16: "DISCHARGE", | |
17: "PV + DISCHARGE", | |
18: "GRID + DISCHARGE", | |
19: "PASSIVE", | |
20: "OFF", | |
21: "OWN CONSUMPTION", | |
22: "RESTART", | |
23: "MAN. EQUALIZATION: CHARGE", | |
24: "MAN. DESULFATATION: CHARGE", | |
25: "SAFETY CHARGE", | |
26: "BATTERY PROTECTION MODE", | |
27: "EG ERROR", | |
28: "EG CHARGE", | |
29: "EG DISCHARGE", | |
30: "EG PASSIVE", | |
31: "EG PROHIBIT CHARGE", | |
32: "EG PROHIBIT DISCHARGE", | |
33: "EMERGANCY CHARGE", | |
34: "SOFTWARE UPDATE", | |
35: "NSP ERROR", | |
36: "NSP ERROR: GRID", | |
37: "NSP ERROR: HARDWRE", | |
38: "NO SERVER CONNECTION", | |
39: "BMS ERROR", | |
40: "MAINTENANCE: FILTER", | |
41: "SLEEPING MODE", | |
42: "WAITING EXCESS", | |
43: "CAPACITY TEST: CHARGE", | |
44: "CAPACITY TEST: DISCHARGE", | |
45: "MAN. DESULFATATION: WAIT", | |
46: "MAN. DESULFATATION: READY", | |
47: "MAN. DESULFATATION: ERROR", | |
48: "EQUALIZATION: WAIT", | |
49: "EMERGANCY CHARGE: ERROR", | |
50: "MAN. EQUALIZATION: WAIT", | |
51: "MAN. EQUALIZATION: ERROR", | |
52: "MAN: EQUALIZATION: READY", | |
53: "AUTO. DESULFATATION: WAIT", | |
54: "ABSORPTION PHASE", | |
55: "DC-SWITCH OFF", | |
56: "PEAK-SHAVING: WAIT", | |
57: "ERROR BATTERY INVERTER", | |
58: "NPU-ERROR", | |
59: "BMS OFFLINE", | |
60: "MAINTENANCE CHARGE ERROR", | |
61: "MAN. SAFETY CHARGE ERROR", | |
62: "SAFETY CHARGE ERROR", | |
63: "NO CONNECTION TO MASTER", | |
64: "LITHIUM SAFE MODE ACTIVE", | |
65: "LITHIUM SAFE MODE DONE", | |
66: "BATTERY VOLTAGE ERROR", | |
67: "BMS DC SWITCHED OFF", | |
68: "GRID INITIALIZATION", | |
69: "GRID STABILIZATION", | |
70: "REMOTE SHUTDOWN", | |
71: "OFFPEAK-CHARGE", | |
72: "ERROR HALFBRIDGE", | |
73: "BMS: ERROR OPERATING TEMPERATURE", | |
74: "FACOTRY SETTINGS NOT FOUND", | |
75: "BACKUP POWER MODE - ACTIVE", | |
76: "BACKUP POWER MODE - BATTERY EMPTY", | |
77: "BACKUP POWER MODE ERROR", | |
78: "INITIALISING", | |
79: "INSTALLATION MODE", | |
80: "GRID OFFLINE", | |
81: "BMS UPDATE NEEDED", | |
82: "BMS CONFIGURATION NEEDED", | |
83: "INSULATION TEST", | |
84: "SELFTEST", | |
85: "EXTERNAL CONTROL", | |
86: "ERROR: TEMPERATURESENSOR", | |
87: "GRID OPERATOR: CHARGE PROHIBITED", | |
88: "GRID OPERATOR: DISCHARGE PROHIBITED", | |
89: "SPARE CAPACITY", | |
90: "SELFTEST ERROR", | |
91: "EARTH FAULT", | |
92: "PV-MODE", | |
93: "REMOTE DISCONNECTION", | |
94: "ERROR DRM0", | |
95: "BATTERY DIAGNOSIS" | |
} | |
if __name__ == "__main__": | |
api = Senec("IP_OF_YOUR_SENEC_APPLIANCE") | |
print(api.get_values()) |
Hi, i used your code here https://github.com/Hobbyflyer/senec_monitoring?fbclid=IwAR0M8O4uSQU_D8rc3BlnNGvXKUCEnsg35lNrw3gdwCu6ia0L5uuLQLbgMEM
Hope it ok...
Hi, i used your code here https://github.com/Hobbyflyer/senec_monitoring?fbclid=IwAR0M8O4uSQU_D8rc3BlnNGvXKUCEnsg35lNrw3gdwCu6ia0L5uuLQLbgMEM
Hope it ok...
Yes of course :)
Hope it works well for you!
Hi Nicolas,
Thanks a lot for your work. I have a question though:
Is it intended to create JSON output? If so, JSON Lint at https://jsonlint.com/ qualified output as "invalid" as according to JSON syntax (https://www.json.org/json-en.html) keys must be enclosed in double instead of single quotes.
Importing json at the beginning
import json
and replacing the end of script
if __name__ == "__main__":
api = Senec("THE_IP_ADDRESS")
s = json.dumps( api.get_values() )
print (s)
created valid JSON according to above linter. If desired, your code certainly might offer better ways of mentioned transformation.
What do you think?
Greetings,
Jörg
Hi Jörg,
sorry for the late answer.
The __main__
part of the file is just for demonstration purposes. I assume this file to be used within other projects where you would just instantiate Senec
and use get_values()
to further process the data. In such a case a Python dict is the right thing to return.
Greetings,
Nico
Hi Nico,
thanks for your feedback - I totally agree!
Greetings,
Jörg
Used your code for smart charger integration script. Thanks for the useful library! (New to Python, literally just coded everything with the help of ChatGPT over the last day :P)
With the latest update (REVISION MCU: 3825; REVISION MCU-BL: 2307; REVISION NPU-REGS: 10; REVISION NPU-IMAGE: 2307; REVISION GUI: 2966) a few things seem to have happened:
- the access to http was switched off, only https works --> line 33 should be changed to:
self.read_api = f"https://{device_ip}/lala.cgi"
- the ssl certificate is not verifiable --> I hope SENEC fixes this. for now as a work around line 38 can be changed to:
response = requests.post(self.read_api, json=request_json, verify=False)
- I noticed that all "'STATISTIC': " Objects are no longer available
Hi, it appears that some other projects that read SENEC's data has managed to find a workaround for the changes in SENEC's firmware that now mandates https.
Would be wonderful if this amazing library can be updated to incorporate this fix too.
Thx @phorbi and @changyang1230
Did a little patch release to make this work again. Now uses HTTPS to connect to Senec appliance and ignores STATISTICS values.
Thank you so so much!
Is that duplicate of BAT1OBJ2 in request_json intentional? Doesn't seem impact anything, but looks redundant.