diff --git a/constants.py b/constants.py index 4c5fb00..b925e55 100644 --- a/constants.py +++ b/constants.py @@ -5,3 +5,16 @@ mqtt_huidig_zon_topic = "HuidigZonWatt" mqtt_huidig_zonA_topic = "HuidigZonA" mqtt_dag_zon_topic = "DagZonWh" mqtt_broker_ip = "192.168.200.26" +solaredge_api_key = "XRUZJIR5AHJCDCOPZLYPC1XMR2W6XRBI" +solaredge_site_id = 457767 +solaredge_monitoring_api_url = f'https://monitoringapi.solaredge.com/site/{solaredge_site_id}/energy' +linux_root = r'/mnt/ftpserver/solaredgedata' +windows_root = r'd:\temp\solaredgedata' +daily_data_file = r'dailysolaredgedata.csv' +monthly_data_file = r'monthlysolaredgedata.csv' +yearly_data_file = r'yearlysolaredgedata.csv' + +# --- Configuratie --- +INVERTER_IP = "192.168.200.65" +INVERTER_PORT = 502 # Probeer 1502 als 502 niet werkt +TOPIC_PREFIX = "solaredge" diff --git a/service_helpers.py b/service_helpers.py new file mode 100644 index 0000000..4cd40ca --- /dev/null +++ b/service_helpers.py @@ -0,0 +1,68 @@ +import csv +import os + +import requests +from paho.mqtt import client as mqtt + +from constants import solaredge_api_key, solaredge_monitoring_api_url, mqtt_username, mqtt_password, mqtt_broker_ip +import logging + + +logger = logging.getLogger() + + +def ensure_dir(file_name): + """ Check/create the folder implied by the specified file. + + If the directory does not exist it will be created. + :param file_name: basestring + """ + d = os.path.dirname(file_name) + if not os.path.exists(d): + os.makedirs(d) + + +def save_data_to_csv(file_name, data): + ensure_dir(file_name) + with open(file_name, mode='w', newline='') as file: + writer = csv.writer(file) + # Kolomkoppen (headers) + writer.writerow(['Datum', 'Energie (Wh)', 'Energie (kWh)']) + for entry in data: + datum = entry['date'] + waarde_wh = entry.get('value', 0) + if waarde_wh is not None: + waarde_kwh = waarde_wh / 1000 + writer.writerow([datum, waarde_wh, waarde_kwh]) + + +def get_energy_totals(time_unit, start_date, end_date): + params = { + 'api_key': solaredge_api_key, + 'startDate': start_date, + 'endDate': end_date, + 'timeUnit': time_unit # Opties: DAY, MONTH, YEAR + } + + response = requests.get(solaredge_monitoring_api_url, params=params) + response.raise_for_status() + + full_json = response.json() + # Stap voor stap door de JSON structuur graven + energy_obj = full_json.get('energy', {}) + values_list = energy_obj.get('values', []) + return values_list + + +def connect_mqtt(): + c = mqtt.Client() + c.username_pw_set(mqtt_username, mqtt_password) + try: + # Stel in: probeer elke 1 tot 120 seconden opnieuw te verbinden + c.reconnect_delay_set(min_delay=1, max_delay=120) + c.connect(mqtt_broker_ip, 1883, 60) + c.loop_start() # Draait op de achtergrond en regelt de automatische reconnect + return c + except Exception as e: + logger.error(f"MQTT Verbinding mislukt: {e}") + return None diff --git a/solar_edge_main.py b/solar_edge_main.py index 8c3225c..11e9951 100644 --- a/solar_edge_main.py +++ b/solar_edge_main.py @@ -1,74 +1,146 @@ import sunspec2.modbus.client as client -import paho.mqtt.client as mqtt import time +import os +from datetime import datetime import logging -from constants import mqtt_username, mqtt_password - -# --- Configuratie --- -INVERTER_IP = "192.168.200.65" -INVERTER_PORT = 502 # Probeer 1502 als 502 niet werkt -MQTT_BROKER = "10.0.0.3" -TOPIC_PREFIX = "solaredge/inverter" +from constants import ( + INVERTER_IP, INVERTER_PORT, TOPIC_PREFIX, windows_root, linux_root, + daily_data_file, monthly_data_file, yearly_data_file +) +from service_helpers import save_data_to_csv, get_energy_totals, connect_mqtt # Logging instellen voor foutopsporing logging.basicConfig(level=logging.INFO) logger = logging.getLogger("SolarEdgeService") -def connect_mqtt(): - c = mqtt.Client() - c.username_pw_set(mqtt_username, mqtt_password) - try: - c.connect(MQTT_BROKER, 1883, 60) - return c - except Exception as e: - logger.error(f"MQTT Verbinding mislukt: {e}") - return None +class SolarEdgeReader: + def __init__(self): + self.daily_data_path = None + self.monthly_data_path = None + self.yearly_data_path = None -def read_and_publish(): - inv_device = None - mqtt_client = connect_mqtt() + def set_data_paths(self): + jaar_str = datetime.now().strftime('%Y') + if os.name == 'nt': + data_root = f"{windows_root}\\{jaar_str}" + else: + data_root = f"{linux_root}/{jaar_str}" - while True: + self.daily_data_path = os.path.join(data_root, daily_data_file) + self.monthly_data_path = os.path.join(data_root, monthly_data_file) + self.yearly_data_path = os.path.join(data_root, yearly_data_file) + + + def get_current_energy_totals_from_solaredge_portal(self): + daily_data, monthly_data, yearly_data = None, None, None try: - # 1. Verbind met Inverter als dat nog niet is gebeurd - if inv_device is None: - logger.info("Verbinden met SolarEdge...") - inv_device = client.SunSpecModbusClientDeviceTCP( - slave_id=1, ipaddr=INVERTER_IP, ipport=INVERTER_PORT, timeout=5 - ) - inv_device.scan() + vandaag = datetime.now().strftime('%Y-%m-%d') + start_maand = datetime.now().strftime('%Y-%m-01') + start_jaar = datetime.now().strftime('%Y-01-01') + start_install = datetime.now().strftime('2017-01-01') + daily_data = get_energy_totals('DAY', start_jaar, vandaag) + if daily_data is not None: + save_data_to_csv(self.daily_data_path, daily_data) - # 2. Data lezen - # Model 101/103 bevatten de inverter data - # We zoeken dynamisch naar het juiste model - itf = inv_device.inverter[0] - itf.read() - - power_w = itf.W.get_value() - energy_total = itf.WH.get_value() - status = itf.St.get_value() - - # 3. Publiceren naar MQTT - if mqtt_client: - mqtt_client.publish(f"{TOPIC_PREFIX}/power", power_w) - mqtt_client.publish(f"{TOPIC_PREFIX}/total_energy", energy_total) - mqtt_client.publish(f"{TOPIC_PREFIX}/status", status) - logger.info(f"Update verzonden: {power_w}W total_energy: {energy_total}Wh status: {status}") - else: - mqtt_client = connect_mqtt() # Herstel MQTT verbinding + monthly_data = get_energy_totals('MONTH', start_install, vandaag) + if monthly_data is not None: + save_data_to_csv(self.monthly_data_path, monthly_data) + yearly_data = get_energy_totals('YEAR', start_install, vandaag) + if yearly_data is not None: + save_data_to_csv(self.yearly_data_path, yearly_data) except Exception as e: - logger.error(f"Fout tijdens uitlezen: {e}") - # Forceer nieuwe verbinding bij de volgende poging - if inv_device: - inv_device.close() - inv_device = None - time.sleep(5) # Korte pauze bij fout om CPU te sparen + logger.error(f"Fout retrieving solaredge totals: {e}") + + return daily_data, monthly_data, yearly_data + + def read_and_publish(self): + inv_device = None + mqtt_client = connect_mqtt() # runs a thread in the background to call loop() automatically. + # This frees up the main thread for other work that may be blocking. + # This call also handles reconnecting to the broker. + # Call loop_stop() to stop the background thread. + start_energy_total_wh = None + start_dag = None + start_maand = None + start_jaar = None + current_date = datetime.now() + prev_date = None + dag_total = 0 + month_total = 0 + year_total = 0 + laatste_check = None + + try: + while True: + try: + huidige_datum = datetime.now().date() + day_changed = laatste_check is None or huidige_datum > laatste_check + # 1. Verbind met Inverter als dat nog niet is gebeurd + if inv_device is None: + logger.info("Verbinden met SolarEdge...") + inv_device = client.SunSpecModbusClientDeviceTCP( + slave_id=1, ipaddr=INVERTER_IP, ipport=INVERTER_PORT, timeout=5 + ) + inv_device.scan() + + # 2. Data lezen + # Model 101/103 bevatten de inverter data + # We zoeken dynamisch naar het juiste model + itf = inv_device.inverter_single_phase[0] + itf.read() + # odict_keys(['ID', 'L', 'A', 'AphA', 'AphB', 'AphC', 'A_SF', 'PPVphAB', 'PPVphBC', 'PPVphCA', 'PhVphA', 'PhVphB', 'PhVphC', 'V_SF', 'W', 'W_SF', 'Hz', 'Hz_SF', 'VA', 'VA_SF', 'VAr', 'VAr_SF', 'PF', 'PF_SF', 'WH', 'WH_SF', 'DCA', 'DCA_SF', 'DCV', 'DCV_SF', 'DCW', 'DCW_SF', 'TmpCab', 'TmpSnk', 'TmpTrns', 'TmpOt', 'Tmp_SF', 'St', 'StVnd', 'Evt1', 'Evt2', 'EvtVnd1', 'EvtVnd2', 'EvtVnd3', 'EvtVnd4']) + power_w = itf.W.get_value() / 100.0 + energy_total_wh = itf.WH.get_value() /100.0 + status = itf.St.get_value() + if start_energy_total_wh is None or day_changed: + self.set_data_paths() + start_energy_total_wh = energy_total_wh + logger.info("Retrieving totals from solaredge api") + daily_data, monthly_data, yearly_data = self.get_current_energy_totals_from_solaredge_portal() + start_dag = daily_data[-1].get('value', 0) + start_maand = monthly_data[-1].get('value', 0) + start_jaar = yearly_data[-1].get('value', 0) + dag_total = start_dag + month_total = start_maand + year_total = start_jaar + laatste_check = huidige_datum + else: + delta_energy_total_wh_dag = energy_total_wh - start_energy_total_wh + dag_total = start_dag + delta_energy_total_wh_dag + month_total = start_maand + delta_energy_total_wh_dag + year_total = start_jaar + delta_energy_total_wh_dag + logger.info(f"dag_totaal={dag_total/1000.0}kWh, month_total={month_total/1000.0}kWh, year_total={year_total/1000.0}kWh") + + # 3. Publiceren naar MQTT + if mqtt_client: + mqtt_client.publish(f"{TOPIC_PREFIX}/power", power_w) + mqtt_client.publish(f"{TOPIC_PREFIX}/total_day_kwh", dag_total/1000.0) + mqtt_client.publish(f"{TOPIC_PREFIX}/total_month_kwh", month_total / 1000.0) + mqtt_client.publish(f"{TOPIC_PREFIX}/total_year_kwh", year_total / 1000.0) + mqtt_client.publish(f"{TOPIC_PREFIX}/total_energy_kWh", energy_total_wh/1000.0) + mqtt_client.publish(f"{TOPIC_PREFIX}/status", status) + logger.info(f"Update verzonden: Current power: {power_w}W total_day_kwh: {dag_total/1000.0}kWh status: {status}") + else: + mqtt_client = connect_mqtt() # Herstel MQTT verbinding + + except Exception as e: + logger.error(f"Fout tijdens uitlezen: {e}") + # Forceer nieuwe verbinding bij de volgende poging + if inv_device: + inv_device.close() + inv_device = None + time.sleep(5) # Korte pauze bij fout om CPU te sparen + + time.sleep(15) # Wacht 15 seconden voor de volgende update + finally: + if mqtt_client: + mqtt_client.loop_stop() - time.sleep(15) # Wacht 15 seconden voor de volgende update if __name__ == "__main__": - read_and_publish() + reader = SolarEdgeReader() + reader.read_and_publish()