Skip to content

Commit e543613

Browse files
committed
Support MQTT Broker, e.g. for HomeAsisstant, ioBroker
1 parent beda98e commit e543613

File tree

4 files changed

+216
-10
lines changed

4 files changed

+216
-10
lines changed

README.md

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
1-
- [SolisCloud to PVOutput and/or Domoticz](#soliscloud-to-pvoutput-andor-domoticz)
1+
- [SolisCloud to PVOutput, Domoticz and/or MQTT Broker (e.g. HomeAssistant, ioBroker)](#soliscloud-to-pvoutput-domoticz-andor-mqtt-broker-eg-homeassistant-iobroker)
22
- [SolisCloud](#soliscloud)
33
- [PVOutput](#pvoutput)
44
- [Domoticz](#domoticz)
5+
- [MQTT Broker (e.g. HomeAssistant, ioBroker)](#mqtt-broker-eg-homeassistant-iobroker)
56
- [Configuration](#configuration)
67
- [Usage: Windows 10](#usage-windows-10)
78
- [Usage: Linux or Raspberry pi](#usage-linux-or-raspberry-pi)
@@ -11,8 +12,8 @@
1112
- [Combined data of two PVOutput accounts/inverters](#combined-data-of-two-pvoutput-accountsinverters)
1213
- [Example standard output of SolisCloud2PVoutput](#example-standard-output-of-soliscloud2pvoutput)
1314

14-
# SolisCloud to PVOutput and/or Domoticz
15-
Simple Python3 script to copy latest (normally once per 5 minutes) SolisCloud portal inverter update to PVOutput portal and/or Domoticz.
15+
# SolisCloud to PVOutput, Domoticz and/or MQTT Broker (e.g. HomeAssistant, ioBroker)
16+
Simple Python3 script to copy latest (normally once per 5 minutes) SolisCloud portal inverter update to PVOutput portal, Domoticz, and/or MQTT Broker (e.g. HomeAssistant, ioBroker).
1617

1718
The soliscloud_to_pvoutput.py script will get the station id via the configured soliscloud_station_index (default the first station) with the secrets of SolisCloud (see next section). Thereafter it will get the inverter id and serial number via the configured soliscloud_inverter_index (default the first inverter). Then in an endless loop the inverter details are fetched and the following information is used:
1819
* timestamp
@@ -66,8 +67,13 @@ If you want to know how to configure in Domoticz your inverter, see [this discus
6667

6768
![alt text](https://user-images.githubusercontent.com/17342657/237064582-59fcd74b-5b04-4578-98a4-18819bf8482f.png)
6869

70+
## MQTT Broker (e.g. HomeAssistant, ioBroker)
71+
An MQTT broker is a server that receives all messages from the clients and then routes the messages to the appropriate destination clients. Information is organized in a hierarchy of topics. When SolisCloud2PVOutput has a new item of data to distribute, it sends a control message with the data to the connected broker. The broker then distributes the information to any clients that have subscribed to that topic. The SolisCloud2PVOutput does not need to have any data on the number or locations of subscribers, and subscribers, in turn, do not have to be configured with any data about the publishers.
72+
73+
If you want to know how to configure your inverter to send information to a MQTT Broker, see [this discussion](https://github.com/ZuinigeRijder/SolisCloud2PVOutput/discussions/30).
74+
6975
# Configuration
70-
Change in soliscloud_to_pvoutput.cfg the following lines with your above obtained secrets and domoticz configuration, including if you want to send to PVOutput, Domoticz or both. By default only output is send to PVOutput:
76+
Change in soliscloud_to_pvoutput.cfg the following lines with your above obtained secrets, domoticz configuration, mqtt configuration, including if you want to send information to PVOutput, Domoticz, MQTT or a combination of those. By default only output is send to PVOutput:
7177
````
7278
[api_secrets]
7379
soliscloud_api_id = 1300386381123456789
@@ -99,6 +105,25 @@ domot_batterypower_id = 0
99105
domot_gridpower_id = 0
100106
domot_familyloadpower_id = 0
101107
domot_homeconsumption_id = 0
108+
109+
[MQTT]
110+
send_to_mqtt = False
111+
mqtt_broker_hostname = localhost
112+
mqtt_broker_port = 1883
113+
mqtt_broker_username =
114+
mqtt_broker_password =
115+
mqtt_main_topic = SolisCloud2PVOutput
116+
mqtt_last_update_id = last_update
117+
mqtt_power_generated_id = power_generated
118+
mqtt_ac_volt_id = ac_volt
119+
mqtt_inverter_temp_id = inverter_temp
120+
mqtt_volt_id = volt
121+
mqtt_solarpower_id = solarpower
122+
mqtt_energygeneration_id = energygeneration
123+
mqtt_batterypower_id = batterypower
124+
mqtt_gridpower_id = gridpower
125+
mqtt_familyloadpower_id = familyloadpower
126+
mqtt_homeconsumption_id = homeconsumption
102127
````
103128

104129
Because I see some forks or local adaptions for people wanting a slightly different behavior, I made some adaptions to the SolisCloud2PVOutput solution and configuration to capture (some of) those variations.
@@ -113,7 +138,7 @@ Note 1: for the last bullet, you need to have a [Solis Consumption Monitoring so
113138

114139
Note 2: make sure that you move send_to_pvoutput setting to the [PVOutput] section, if you have an already existing configuration.
115140

116-
141+
Note 3: mqtt_broker_username and mqtt_broker_password are optional
117142

118143
# Usage: Windows 10
119144
Make sure to go to the directory where soliscloud_to_pvoutput.py and soliscloud_to_pvoutput.cfg is located.

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
paho_mqtt>=1.6.1

soliscloud_to_pvoutput.cfg

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,4 +27,23 @@ domot_energygeneration_id = 0
2727
domot_batterypower_id = 0
2828
domot_gridpower_id = 0
2929
domot_familyloadpower_id = 0
30-
domot_homeconsumption_id = 0
30+
domot_homeconsumption_id = 0
31+
32+
[MQTT]
33+
send_to_mqtt = False
34+
mqtt_broker_hostname = localhost
35+
mqtt_broker_port = 1883
36+
mqtt_broker_username =
37+
mqtt_broker_password =
38+
mqtt_main_topic = SolisCloud2PVOutput
39+
mqtt_last_update_id = last_update
40+
mqtt_power_generated_id = power_generated
41+
mqtt_ac_volt_id = ac_volt
42+
mqtt_inverter_temp_id = inverter_temp
43+
mqtt_volt_id = volt
44+
mqtt_solarpower_id = solarpower
45+
mqtt_energygeneration_id = energygeneration
46+
mqtt_batterypower_id = batterypower
47+
mqtt_gridpower_id = gridpower
48+
mqtt_familyloadpower_id = familyloadpower
49+
mqtt_homeconsumption_id = homeconsumption

soliscloud_to_pvoutput.py

Lines changed: 165 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import hashlib
99
import hmac
1010
import json
11+
import random
1112
import time
1213
import sys
1314
import configparser
@@ -20,8 +21,24 @@
2021
from urllib.error import HTTPError, URLError
2122
from urllib.request import urlopen, Request
2223

24+
from paho.mqtt import client as mqtt_client
25+
26+
27+
def arg_has(string: str) -> bool:
28+
"""arguments has string"""
29+
for i in range(1, len(sys.argv)):
30+
if sys.argv[i].lower() == string:
31+
return True
32+
return False
33+
34+
2335
# == read api_secrets in soliscloud_to_pvoutput.cfg ==========================
2436
SCRIPT_DIRNAME = path.abspath(path.dirname(__file__))
37+
logging.config.fileConfig(f"{SCRIPT_DIRNAME}/logging_config.ini")
38+
D = arg_has("debug")
39+
if D:
40+
logging.getLogger().setLevel(logging.DEBUG)
41+
2542
parser = configparser.ConfigParser()
2643
parser.read(f"{SCRIPT_DIRNAME}/soliscloud_to_pvoutput.cfg")
2744

@@ -51,6 +68,8 @@ def get_bool(dictionary: dict, key: str, default: bool = True) -> bool:
5168
PVOUTPUT_API_KEY = get(api_secrets, "pvoutput_api_key")
5269
PVOUTPUT_SYSTEM_ID = get(api_secrets, "pvoutput_system_id")
5370

71+
SOLISCLOUD_INVERTER_SN = "SN" # to be filled later by program
72+
5473
# == PVOutput info, fill in yours in soliscloud_to_pvoutput.cfg ===========
5574
pvoutput_info = dict(parser.items("PVOutput"))
5675
SEND_TO_PVOUTPUT = get_bool(pvoutput_info, "send_to_pvoutput") # default True
@@ -87,6 +106,32 @@ def get_bool(dictionary: dict, key: str, default: bool = True) -> bool:
87106
DOMOTICZ_FAMILYLOADPOWER_ID = get(domoticz_info, "domot_familyloadpower_id", "0")
88107
DOMOTICZ_HOMECONSUMPTION_ID = get(domoticz_info, "domot_homeconsumption_id", "0")
89108

109+
# == mqtt info, fill in yours in soliscloud_to_pvoutput.cfg ===========
110+
mqtt_info = dict(parser.items("MQTT"))
111+
SEND_TO_MQTT = get_bool(mqtt_info, "send_to_mqtt", False)
112+
MQTT_BROKER_HOSTNAME = get(mqtt_info, "mqtt_broker_hostname", "localhost")
113+
MQTT_BROKER_PORT = int(get(mqtt_info, "mqtt_broker_port", "1883"))
114+
MQTT_BROKER_USERNAME = get(mqtt_info, "mqtt_broker_username", "")
115+
MQTT_BROKER_PASSWORD = get(mqtt_info, "mqtt_broker_password", "")
116+
117+
MQTT_MAIN_TOPIC = get(mqtt_info, "mqtt_main_topic", "soliscloud2pvoutput")
118+
119+
MQTT_LAST_UPDATE_ID = get(mqtt_info, "mqtt_last_update_id", "last_update")
120+
MQTT_POWER_GENERATED_ID = get(mqtt_info, "mqtt_power_generated_id", "power_generated")
121+
MQTT_AC_VOLT_ID = get(mqtt_info, "mqtt_ac_volt_id", "ac_volt")
122+
MQTT_INVERTER_TEMP_ID = get(mqtt_info, "mqtt_inverter_temp_id", "inverter_temp")
123+
MQTT_VOLT_ID = get(mqtt_info, "mqtt_volt_id", "volt")
124+
MQTT_SOLARPOWER_ID = get(mqtt_info, "mqtt_solarpower_id", "solarpower")
125+
MQTT_ENERGYGENERATION_ID = get(
126+
mqtt_info, "mqtt_energygeneration_id", "energygeneration"
127+
)
128+
MQTT_BATTERYPOWER_ID = get(mqtt_info, "mqtt_batterypower_id", "batterypower")
129+
MQTT_GRIDPOWER_ID = get(mqtt_info, "mqtt_gridpower_id", "gridpower")
130+
MQTT_FAMILYLOADPOWER_ID = get(mqtt_info, "mqtt_familyloadpower_id", "familyloadpower")
131+
MQTT_HOMECONSUMPTION_ID = get(mqtt_info, "mqtt_homeconsumption_id", "homeconsumption")
132+
133+
MQTT_CLIENT = None # will be filled at MQTT connect if configured
134+
90135
# == Constants ===============================================================
91136
VERB = "POST"
92137
CONTENT_TYPE = "application/json"
@@ -98,8 +143,6 @@ def get_bool(dictionary: dict, key: str, default: bool = True) -> bool:
98143

99144
TODAY = datetime.now().strftime("%Y%m%d") # format yyyymmdd
100145

101-
logging.config.fileConfig(f"{SCRIPT_DIRNAME}/logging_config.ini")
102-
103146

104147
# == post ====================================================================
105148
def execute_request(url: str, data: str, headers: dict) -> str:
@@ -209,9 +252,93 @@ def send_to_domoticz(idx: str, value: str):
209252
return
210253

211254

255+
# == connect MQTT ========================================================
256+
def connect_mqtt():
257+
"""connect_mqtt"""
258+
259+
mqtt_first_reconnect_delay = 1
260+
mqtt_reconnect_rate = 2
261+
mqtt_max_reconnect_count = 12
262+
mqtt_max_reconnect_delay = 60
263+
264+
def on_connect(client, userdata, flags, rc): # pylint: disable=unused-argument
265+
if rc == 0:
266+
logging.debug("Connected to MQTT Broker!")
267+
else:
268+
logging.error("Failed to connect to MQTT Broker, return code %d\n", rc)
269+
270+
def on_disconnect(client, userdata, rc): # pylint: disable=unused-argument
271+
logging.info("Disconnected with result code: %s", rc)
272+
reconnect_count = 0
273+
reconnect_delay = mqtt_first_reconnect_delay
274+
while reconnect_count < mqtt_max_reconnect_count:
275+
logging.info("Reconnecting in %d seconds...", reconnect_delay)
276+
time.sleep(reconnect_delay)
277+
278+
try:
279+
client.reconnect()
280+
logging.info("Reconnected successfully!")
281+
return
282+
except Exception as reconnect_ex: # pylint: disable=broad-except
283+
logging.error("%s. Reconnect failed. Retrying...", reconnect_ex)
284+
285+
reconnect_delay *= mqtt_reconnect_rate
286+
reconnect_delay = min(reconnect_delay, mqtt_max_reconnect_delay)
287+
reconnect_count += 1
288+
logging.info("Reconnect failed after %s attempts. Exiting...", reconnect_count)
289+
290+
mqtt_client_id = (
291+
f"{MQTT_MAIN_TOPIC}-{SOLISCLOUD_INVERTER_SN}-{random.randint(0, 1000)}"
292+
)
293+
client = mqtt_client.Client(mqtt_client_id)
294+
client.on_connect = on_connect
295+
client.on_disconnect = on_disconnect
296+
if MQTT_BROKER_USERNAME and MQTT_BROKER_PASSWORD:
297+
client.username_pw_set(MQTT_BROKER_USERNAME, MQTT_BROKER_PASSWORD)
298+
client.connect(MQTT_BROKER_HOSTNAME, MQTT_BROKER_PORT)
299+
return client
300+
301+
302+
# == send to MQTT ========================================================
303+
def send_to_mqtt(subtopic: str, value: str):
304+
"""send_to_mqtt"""
305+
msg_count = 1
306+
topic = f"{MQTT_MAIN_TOPIC}/{SOLISCLOUD_INVERTER_SN}/{subtopic}"
307+
msg = f"{value}"
308+
logging.info( # pylint:disable=logging-fstring-interpolation
309+
f"topic: {topic}, msg: {msg}"
310+
)
311+
while True:
312+
try:
313+
error = False
314+
result = MQTT_CLIENT.publish(topic, msg, qos=1, retain=True)
315+
status = result[0]
316+
if status == 0:
317+
msg_count = 6
318+
else:
319+
error = True
320+
except Exception as publish_ex: # pylint: disable=broad-except
321+
logging.error( # pylint:disable=logging-fstring-interpolation
322+
f"MQTT publish Exception: {publish_ex}, sleeping a minute"
323+
)
324+
traceback.print_exc()
325+
time.sleep(60)
326+
327+
if error:
328+
logging.error( # pylint:disable=logging-fstring-interpolation
329+
f"Failed to send message {msg} to topic {topic}"
330+
)
331+
time.sleep(1)
332+
msg_count += 1
333+
334+
if msg_count > 5:
335+
break
336+
337+
212338
# == get_inverter_list_body ==================================================
213339
def get_inverter_list_body() -> str:
214340
"""get inverter list body"""
341+
global SOLISCLOUD_INVERTER_SN # pylint: disable=global-statement
215342
body = '{"userid":"' + SOLISCLOUD_API_ID + '"}'
216343
content = get_solis_cloud_data(USER_STATION_LIST, body)
217344
station_info = json.loads(content)["data"]["page"]["records"][
@@ -226,6 +353,7 @@ def get_inverter_list_body() -> str:
226353
]
227354
inverter_id = inverter_info["id"]
228355
inverter_sn = inverter_info["sn"]
356+
SOLISCLOUD_INVERTER_SN = inverter_sn
229357

230358
body = '{"id":"' + inverter_id + '","sn":"' + inverter_sn + '"}'
231359
logging.info("body: %s", body)
@@ -235,7 +363,20 @@ def get_inverter_list_body() -> str:
235363
# == do_work ====================================================================
236364
def do_work():
237365
"""do_work"""
366+
global MQTT_CLIENT # pylint:disable=global-statement
238367
inverter_detail_body = get_inverter_list_body()
368+
if SEND_TO_MQTT:
369+
while True:
370+
try:
371+
MQTT_CLIENT = connect_mqtt()
372+
MQTT_CLIENT.loop_start()
373+
break
374+
except Exception as connect_ex: # pylint: disable=broad-except
375+
logging.error( # pylint:disable=logging-fstring-interpolation
376+
f"MQTT connect Exception: {connect_ex}, sleeping a minute"
377+
)
378+
traceback.print_exc()
379+
time.sleep(60)
239380
timestamp_previous = "0"
240381
energy_generation = 0
241382
while True:
@@ -350,6 +491,23 @@ def do_work():
350491
send_to_domoticz(DOMOTICZ_GRIDPOWER_ID, str(grid_power))
351492
send_to_domoticz(DOMOTICZ_FAMILYLOADPOWER_ID, str(family_load))
352493
send_to_domoticz(DOMOTICZ_HOMECONSUMPTION_ID, str(home_consumption))
494+
495+
if SEND_TO_MQTT:
496+
send_to_mqtt(MQTT_LAST_UPDATE_ID, f"{TODAY} {current_time}")
497+
send_to_mqtt(
498+
MQTT_POWER_GENERATED_ID,
499+
str(solar_power) + ";" + str(energy_generation),
500+
)
501+
send_to_mqtt(MQTT_AC_VOLT_ID, ac_voltage)
502+
send_to_mqtt(MQTT_INVERTER_TEMP_ID, inverter_temperature)
503+
send_to_mqtt(MQTT_VOLT_ID, dc_voltage)
504+
send_to_mqtt(MQTT_SOLARPOWER_ID, str(solar_power))
505+
send_to_mqtt(MQTT_ENERGYGENERATION_ID, str(energy_generation))
506+
send_to_mqtt(MQTT_BATTERYPOWER_ID, str(battery_power))
507+
send_to_mqtt(MQTT_GRIDPOWER_ID, str(grid_power))
508+
send_to_mqtt(MQTT_FAMILYLOADPOWER_ID, str(family_load))
509+
send_to_mqtt(MQTT_HOMECONSUMPTION_ID, str(home_consumption))
510+
353511
timestamp_previous = timestamp_current
354512

355513

@@ -361,13 +519,16 @@ def main_loop():
361519
do_work()
362520
logging.info("Progam finished successful")
363521
finished = True
364-
except Exception as exception: # pylint: disable=broad-except
522+
except Exception as main_loop_ex: # pylint: disable=broad-except
365523
logging.error( # pylint:disable=logging-fstring-interpolation
366-
f"Exception: {exception}, sleeping a minute"
524+
f"Exception: {main_loop_ex}, sleeping a minute"
367525
)
368526
traceback.print_exc()
369527
time.sleep(60)
370528

529+
if SEND_TO_MQTT:
530+
MQTT_CLIENT.loop_stop()
531+
371532

372533
# == MAIN ====================================================================
373534
main_loop()

0 commit comments

Comments
 (0)