def onStart(self): self.add_devices = bool(Parameters["Mode1"]) # Domoticz will generate graphs showing an interval of 5 minutes. # Calculate the number of samples to store over a period of 5 minutes. self.max_samples = 300 / int(Parameters["Mode2"]) # Now set the interval at which the information is collected accordingly. Domoticz.Heartbeat(int(Parameters["Mode2"])) if Parameters["Mode6"] == "Debug": Domoticz.Debugging(1) else: Domoticz.Debugging(0) Domoticz.Debug("onStart Address: {} Port: {}".format( Parameters["Address"], Parameters["Port"])) self.inverter = solaredge_modbus.Inverter(host=Parameters["Address"], port=Parameters["Port"], timeout=3, unit=1) # Lets get in touch with the inverter. self.contactInverter()
result[key] = float( round((10**input['%s_scale' % key.split("_", 2)[1]]) * val, 2)) except: continue except: continue return result #Load all config from file with open('/etc/se_modbus2influx.conf') as config_file: config = json.load(config_file) #Connect to Inverter and InfluxDB inverter = solaredge_modbus.Inverter(host=config["INVERTER_ADDRESS"], port=config["INVERTER_TCP_PORT"]) client = InfluxDBClient(host=config["INFLUXDB_ADDRESS"], port=config["INFLUXDB_PORT"], username=config["INFLUXDB_USER"], password=config["INFLUXDB_PASSWORD"]) # Create InfluxDB data template data = {'measurement': 'SolarEdge_data', 'tags': '', 'fields': ''} tags = {} fields = {} json_body = [] loop_count = 0 #Check if the database already exsists, if not create it databases = client.get_list_database() if 'solaredge_data' not in databases:
def publish(entry, topic, definition): try: inverter = solaredge_modbus.Inverter( host=entry.config['solaredge_modbus_tcp_host'], port=entry.config['solaredge_modbus_tcp_port'], timeout=utils.read_duration( entry.config['solaredge_modbus_tcp_timeout']), unit=entry.config['solaredge_modbus_tcp_unit']) inverter_data = {} values = inverter.read_all() filtered = "energy_total" not in values or "temperature" not in values or ( _float_is_zero(values["energy_total"]) and _float_is_zero(values["temperature"])) if not filtered: if "c_serialnumber" in values: inverter_data["c_serialnumber"] = values["c_serialnumber"] for k, v in values.items(): if not entry.config['solaredge_modbus_tcp_data_filter'] or ( "inverter" not in entry.config['solaredge_modbus_tcp_data_filter'] ) or not entry.config['solaredge_modbus_tcp_data_filter'][ "inverter"] or k in entry.config[ 'solaredge_modbus_tcp_data_filter']["inverter"]: if (isinstance(v, int) or isinstance(v, float)) and "_scale" not in k: k_split = k.split("_") scale = 0 if f"{k_split[len(k_split) - 1]}_scale" in values: scale = values[ f"{k_split[len(k_split) - 1]}_scale"] elif f"{k}_scale" in values: scale = values[f"{k}_scale"] inverter_data.update({k: float(v * (10**scale))}) elif "_scale" not in k: inverter_data.update({k: v}) if (inverter_data): entry.publish('./inverter', inverter_data) meter_data = {} meters = inverter.meters() for meter, params in meters.items(): meter = meter.lower() meter_data[meter] = {} values = params.read_all() filtered = "export_energy_active" not in values or "import_energy_active" not in values or "frequency" not in values or ( _float_is_zero(values["export_energy_active"]) and _float_is_zero(values["import_energy_active"]) and _float_is_zero(values["frequency"])) if not filtered: if "c_serialnumber" in values: meter_data[meter]["c_serialnumber"] = values[ "c_serialnumber"] for k, v in values.items(): if not entry.config['solaredge_modbus_tcp_data_filter'] or ( "meter" not in entry.config['solaredge_modbus_tcp_data_filter'] ) or not entry.config['solaredge_modbus_tcp_data_filter'][ "meter"] or k in entry.config[ 'solaredge_modbus_tcp_data_filter']["meter"]: if (isinstance(v, int) or isinstance(v, float)) and "_scale" not in k: k_split = k.split("_") scale = 0 if f"{k_split[len(k_split) - 1]}_scale" in values: scale = values[ f"{k_split[len(k_split) - 1]}_scale"] elif f"{k}_scale" in values: scale = values[f"{k}_scale"] meter_data[meter].update( {k: float(v * (10**scale))}) elif "_scale" not in k: meter_data[meter].update({k: v}) if meter_data[meter]: entry.publish('./meter/' + meter, meter_data[meter]) battery_data = {} batteries = inverter.batteries() for battery, params in batteries.items(): battery = battery.lower() battery_data[battery] = {} values = params.read_all() filtered = "lifetime_export_energy_counter" not in values or "lifetime_export_energy_counter" not in values or "instantaneous_voltage" not in values or ( _float_is_zero(values["lifetime_export_energy_counter"]) and _float_is_zero(values["lifetime_export_energy_counter"]) and _float_is_zero(values["instantaneous_voltage"])) if not filtered: if "c_serialnumber" in values: battery_data[battery]["c_serialnumber"] = values[ "c_serialnumber"] for k, v in values.items(): if not entry.config['solaredge_modbus_tcp_data_filter'] or ( "battery" not in entry.config['solaredge_modbus_tcp_data_filter'] ) or not entry.config['solaredge_modbus_tcp_data_filter'][ "battery"] or k in entry.config[ 'solaredge_modbus_tcp_data_filter']["battery"]: if (isinstance(v, int) or isinstance(v, float)) and "_scale" not in k: k_split = k.split("_") scale = 0 if f"{k_split[len(k_split) - 1]}_scale" in values: scale = values[ f"{k_split[len(k_split) - 1]}_scale"] elif f"{k}_scale" in values: scale = values[f"{k}_scale"] battery_data[battery].update( {k: float(v * (10**scale))}) elif "_scale" not in k: battery_data[battery].update({k: v}) if battery_data[battery]: entry.publish('./battery/' + battery, battery_data[battery]) except: logging.exception( "{id}> Exception during inverter data collection...".format( id=entry.id))
argparser.add_argument("--timeout", type=int, default=1, help="Connection timeout") argparser.add_argument("--unit", type=int, default=1, help="Modbus device address") argparser.add_argument("--json", action="store_true", default=False, help="Output as JSON") args = argparser.parse_args() inverter = solaredge_modbus.Inverter(host=args.host, port=args.port, timeout=args.timeout, unit=args.unit) values = {} values = inverter.read_all() meters = inverter.meters() batteries = inverter.batteries() values["meters"] = {} values["batteries"] = {} for meter, params in meters.items(): meter_values = params.read_all() values["meters"][meter] = meter_values for battery, params in batteries.items(): battery_values = params.read_all()
def solaredge_main(mqtt_queue: multiprocessing.Queue, config: Dict[str, Any]) -> None: """ Main function for the solaredge process Read messages from modbus, process them and send them to the queue This tries to sync reads from the inverter to the "read_every" config setting. The sync happens before the read from the inverter. This is not ideal, since we don't know how long the read will take. This is a concious tradeoff. """ LOGGER.info("solaredge process starting") inverter = solaredge_modbus.Inverter(host=config["solaredge_host"], port=config["solaredge_port"], timeout=5) # This is a correction term that is used to adjust for the fact # that waking up from sleep takes a while. epsilon = 0 # We want to run execution on seconds that are divisible by this synctime = config["read_every"] # If the scheduled start time and the actual start time is # off by more than this, skip the run maxdelta = 0.05 # This object is purely used to sleep on event = threading.Event() while True: # Time where the next execution is supposed to happen now = time.time() nextrun = math.ceil(now / synctime) * synctime sleep = nextrun - now - epsilon if sleep < 0: # This can happen if we're very close to nextrun, and # epsilon is negative. In this case restart the loop, # which should push us into the next interval LOGGER.error("Skipping loop due to negative sleep interval. If " "this keeps happening, increase --read-every") continue # This is a lot more accurate than time.sleep() event.wait(timeout=sleep) start = time.time() delta = start - nextrun # These are the various times involved here: # # T_1 T_2 T_3 T_4 # | | | | # V V V V # ----------------------------------------------------- # # T_1 is the time where we went to sleep # T_2 is the time were the sleep should have ended. T_2 - T_1 is # the duration we pass to the event.wait() call. # T_3 is the time where we wanted to come out of sleep. T_3 - T_2 # is `epsilon`, and in an ideal world it would be 0. # T_4 is the time where we actually came out of the sleep, this # is the time in `start`. T_4 - T_3 is `delta` # # We want to adjust `epsilon` so that T_3 == T_4. # Calculate the adjustment to the sleep duration. This takes the # existing offset, and corrects it by a fraction of the measured # difference. # # This uses a running average over the last N values, # without actually having to store them. # # Assume epsilon contains the average of the last N # values, and we want to adjust for the current delta. # The amount of time we should have slept is (epsilon + delta). # # The new average is # ( (N - 1) * epsilon ) + epsilon + delta ) / N # # which is the same as # # ( N * epsilon + delta ) / N # # or # # epsilon + ( delta / N ) # # Take the average over the last 10 epsilon = epsilon + (0.1 * delta) if abs(epsilon) > 1: LOGGER.error("Implausible epsilon (>1), resetting to 0") LOGGER.error( "Error occured with start=%f, nextrun=%f, delta %f, epsilon %f", start, nextrun, delta, epsilon, ) epsilon = 0 LOGGER.debug( "Starting loop at %f, desired was %f, delta %f, new epsilon %f", start, nextrun, delta, epsilon, ) if abs(delta) > maxdelta: LOGGER.error("Skipping run, offset too large") continue data = None try: data = inverter.read_all() if data == {}: raise ValueError("No data from inverter") LOGGER.debug("Received values from inverter: %s", data) if "c_serialnumber" not in data: raise KeyError("No serial number in data") # The values, as read from the inverter, need to be scaled # according to a scale factor that's also present in the data. # # This might also fail because the data received is incomplete for scalefactor, fields in SCALEFACTORS.items(): for field in fields: if field in data: data[field] = data[field] * (10**data[scalefactor]) del data[scalefactor] LOGGER.debug("Processed data: %s", data) except (KeyError, ValueError, ConnectionException) as exc: LOGGER.error("Error reading from inverter: %s, data: %s", exc, data) LOGGER.error("Sleeping for 5 seconds") event.wait(timeout=5) continue # Add a time stamp. This is an integer, in milliseconds # since epoch data["solaredge_mqtt_timestamp"] = int( (nextrun - config["time_offset"]) * 1000) try: mqtt_queue.put(data, block=False) except Exception: # Ignore this pass
help="Parity") argparser.add_argument("--baud", type=int, default=False, help="Baud rate") argparser.add_argument("--timeout", type=int, default=1, help="Connection timeout") argparser.add_argument("--unit", type=int, default=1, help="Modbus unit") argparser.add_argument("--json", action="store_true", default=False, help="Output as JSON") args = argparser.parse_args() inverter = solaredge_modbus.Inverter(device=args.device, stopbits=args.stopbits, parity=args.parity, baud=args.baud, timeout=args.timeout, unit=args.unit) values = {} values = inverter.read_all() meters = inverter.meters() batteries = inverter.batteries() values["meters"] = {} values["batteries"] = {} for meter, params in meters.items(): meter_values = params.read_all() values["meters"][meter] = meter_values if args.json:
def main(): """ Main processing loop """ # pylint: disable=global-statement # use of global statement here is required to allow main() to set the value based on passed arguments to the program global DEBUG, INFLUX_PASSWORD # Get the passwords from the plain text keyring keyring.set_keyring(PlaintextKeyring()) mqtt_password = keyring.get_password(MQTT_HOST, MQTT_USER) INFLUX_PASSWORD = keyring.get_password(INFLUX_HOST, INFLUX_USER) try: pid_file = os.environ['PIDFILE'] except: pid_file = "null" args = parse_args() # Setup logging if args.D: DEBUG = True set_logging('debug') logging.debug("Running in debug mode, not writing data") else: DEBUG = False set_logging('info') if os.path.exists(pid_file): logging.error("PID already exists. Is getsolar already running?") logging.error( "Either, stop the running process or remove %s or run with the debug flag set (-D)", pid_file) sys.exit(2) else: write_pid_file(pid_file) # Connect to MQTT m_d = mqtt.Client(MQTT_CLIENT_NAME) m_d.connected_flag = False m_d.error_code = 0 m_d.on_connect = on_connect # bind call back function m_d.on_disconnect = on_disconnect m_d.on_log = on_log m_d.username_pw_set(MQTT_USER, mqtt_password) m_d.connect(MQTT_HOST, int(MQTT_PORT)) m_d.loop_start() retry = MAX_RETRIES while not m_d.connected_flag: if retry == 0: # wait in loop for MAX_RETRIES sys.exit("Connect failed with error", m_d.error_code) else: if m_d.error_code == 5: sys.exit("Authorisation Failure" + mqtt_password) time.sleep(1) retry -= 1 # Connect to two InfluxDB databases # DB 1 = Home Assistant database for one minute logging of power and energy data # DB 2 = Powerlogging for 10s logging of power only d_d = InfluxDBClient(INFLUX_HOST, INFLUX_PORT, INFLUX_USER, INFLUX_PASSWORD, INFLUX_DB_ALL) d_p = InfluxDBClient(INFLUX_HOST, INFLUX_PORT, INFLUX_USER, INFLUX_PASSWORD, INFLUX_DB_POWER) inv_data = InverterData() # Initialise cycle counter and number of retries counter = MAX_COUNTER retry = MAX_RETRIES # Connect to solaredge modbus inverter logging.debug("Connect to device. Host " + args.i + " Port " + str(args.p) + " Timeout " + str(args.t) + " Unit " + str(args.u)) s_d = solaredge_modbus.Inverter(host=args.i, port=args.p, timeout=args.t, unit=args.u) # s_d.connect() # Try up to MAX_RETRIES times to read data from the inverter while retry != 0: if not s_d.connected(): retry -= 1 time.sleep(WAIT_TIME) logging.debug("Retry. Connect to device. Host " + args.i + " Port " + str(args.p) + " Timeout " + str(args.t) + " Unit " + str(args.u)) s_d = solaredge_modbus.Inverter(host=args.i, port=args.p, timeout=args.t, unit=args.u) else: retry = MAX_RETRIES # Read registers logging.debug("Reading data - cycle %s", counter) inv_data.update(s_d) inv_data.write_power(d_p) if counter == 0: inv_data.write_ha(m_d, d_d) counter = 5 else: counter -= 1 time.sleep(SLEEP_TIME) logging.error("Too many retries") rm_pid_file(pid_file) sys.exit(2)