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()
Example #2
0
                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))
Example #4
0
    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()
Example #5
0
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
Example #6
0
                           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:
Example #7
0
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)