Example #1
0
def dump_modbus_reads(args, port):
    """
    Use the caching client and do a full read.
    """
    dump_path = Path(args.dump_path)
    if dump_path.exists():
        os.remove(dump_path)
    with Mate3Client(host=args.host, port=port, cache_path=args.dump_path, cache_only=False) as client:
        client.read_all_modbus_values_unparsed()
        print(
            f"All debug modbus reads are cached in the file '{args.dump_path}'.\nNOTE THAT THIS MAY CONTAIN SENSITIVE "
            "INFORMATION LIKE PASSWORDS ETC."
        )
Example #2
0
def test_known_system(subtests, system_dir):
    # Read it (which is a test in itself)
    with Mate3Client(
        host=None, cache_path=system_dir / "modbus.json", cache_only=True, cache_writeable=False
    ) as client:
        with subtests.test("Reading gives expected results"):
            _test_read_gives_expected_results(client, system_dir)
        with subtests.test("Writing the same values back gives the same results"):
            # ... 'cos if that isn't the case, it implies we're serialising/deserialising incorrectly.
            cache = {k: v for k, v in client._client._cache.items()}
            for device in client.devices.connected_devices:
                # NB: ignoring Mode.W as we can't read the value to be able to write it back i.e. we don't know what to
                # write.
                for value in device.fields(modes=[Mode.RW]):
                    if value.implemented:
                        value.write(value.value)
            new_cache = client._client._cache
            assert check_objects(cache, new_cache)
Example #3
0
def main():
    import sys

    # base parser for shared arguments (which makes sub parsers nicer)
    base_parser = argparse.ArgumentParser(add_help=False)
    base_parser.add_argument("--host",
                             "-H",
                             dest="host",
                             default=None,
                             required=False,
                             help="The host name or IP address of the Mate3")
    base_parser.add_argument("--port",
                             "-p",
                             dest="port",
                             type=int,
                             default=None,
                             required=False,
                             help="The port number address of the Mate3")
    base_parser.add_argument(
        "--loglevel",
        "-l",
        dest="loglevel",
        default="INFO",
        required=False,
        help="Logging level",
        choices=("DEBUG", "INFO", "WARNING", "ERROR"),
    )

    # And some we want to have cache options, and others we don't
    cache_parser = argparse.ArgumentParser(add_help=False)
    cache_parser.add_argument(
        "--cache-path",
        "-c",
        dest="cache_path",
        default=None,
        required=False,
        help="Path to a cache to use instead of host/port.",
    )
    cache_parser.add_argument(
        "--cache-only",
        dest="cache_only",
        action="store_true",
        required=False,
        help="Pass this option if you only want to use the provided cache.",
    )

    parser = argparse.ArgumentParser(
        description="CLI for the Mate3 controller", parents=[base_parser])
    sub_parsers = parser.add_subparsers(dest="cmd",
                                        help="Use mate3 <cmd> -h for help")
    read_parser = sub_parsers.add_parser("read",
                                         help="Read mate3 values",
                                         parents=[base_parser, cache_parser])
    read_parser.add_argument(
        "--format",
        "-f",
        dest="format",
        default="text",
        required=False,
        help="Output format",
        choices=("text", "prettyjson", "json"),
    )
    write_parser = sub_parsers.add_parser("write",
                                          help="Write mate3 values",
                                          parents=[base_parser, cache_parser])
    write_parser.add_argument(
        "--cache-writeable",
        dest="cache_writeable",
        action="store_true",
        required=False,
        help=
        "Pass this option if you want your writes to the cache to persist to disk i.e. update --cache-path.",
    )
    write_parser.add_argument(
        "--set",
        "-s",
        dest="set",
        help=
        ("The field and value to set in the form field=value. For example: "
         "`--set charge_controllers[3].config.absorb_volts=57.6` You can add more than one --set options if you want"
         " to set many fields at once."),
        action="append",
    )
    devices_parser = sub_parsers.add_parser(
        "devices",
        help="List the devices",
        parents=[base_parser, cache_parser])
    dump_modbus_reads_parser = sub_parsers.add_parser(
        "dump", help="Dump a full modbus read", parents=[base_parser])
    dump_modbus_reads_parser.add_argument("dump_path",
                                          help="Path to modbus dump (*.json)")

    if len(sys.argv) == 1:
        parser.print_help()
        sys.exit(1)
    args = parser.parse_args()

    # Set up logging:
    logger.remove()
    logger.add(sys.stderr, level=args.loglevel)

    # Get the client:
    port = Defaults.Port if args.port is None else args.port

    # If dump, let's do it without a normal client:
    if args.cmd == "dump":
        if args.host is None:
            raise RuntimeError("You must specify a host!")
        dump_modbus_reads(args, port)
        exit()

    # All others can use caching, so let's test the cache args:
    assert hasattr(args, "cache_path")
    if args.cache_only and args.cache_path is None:
        raise RuntimeError(
            "You must specify --cache-path if you're using --cache-only")
    if args.cache_path is not None:
        if args.cache_only and (args.host is not None
                                or args.port is not None):
            raise RuntimeError(
                "If using --cache-only, you can't specify a host/port")
    else:
        if args.host is None:
            raise RuntimeError(
                "If not using --cache-path, you must specify a host")

    # Otherwise do some tests and then
    writeable = args.cmd == "write" and args.cache_writeable
    with Mate3Client(host=args.host,
                     port=port,
                     cache_path=args.cache_path,
                     cache_only=args.cache_only,
                     cache_writeable=writeable) as client:
        if args.cmd == "read":
            read(client, args)
        elif args.cmd == "write":
            write(client, args)
        elif args.cmd == "devices":
            list_devices(client)
Example #4
0
CACHE_PATH = Path(
    __file__
).parent.parent / "tests" / "known_systems" / "chinezbrun" / "modbus.json"

if __name__ == "__main__":
    import sys
    import time

    logger.remove()
    logger.add(sys.stderr, level="INFO")

    # Creating a client allows you to interface with the Mate. It also does a read of all devices connected to it (via the
    # hub) on initialisation. The following works on a cached file so you can see how things work without actually
    # accessing/affecting a real Mate3. If you want to try it on your own system (in which case you should remove the
    # writes first!) just replace the line below with `with Mate3Client("<your mate3's IP address>") as client:`
    with Mate3Client(host=None, cache_path=CACHE_PATH,
                     cache_only=True) as client:
        print("# What's the system name?")
        mate = client.devices.mate3
        print(mate.system_name)
        print(
            "# Get the battery voltage. Note that it's auto-scaled appropriately."
        )
        fndc = client.devices.fndc
        print(fndc.battery_voltage)
        print(
            "# Get the (raw) values for the same device type on different ports."
        )
        inverters = client.devices.single_phase_radian_inverters
        for port, inverter in inverters.items():
            print(
                f"Output KW for inverter on port {port} is {inverter.output_kw.value}"
Example #5
0
def main():
    parser = argparse.ArgumentParser(
        description="Read all available data from the Mate3 controller")

    parser.add_argument("--host",
                        "-H",
                        dest="host",
                        help="The host name or IP address of the Mate3",
                        required=True)
    parser.add_argument("--port",
                        "-p",
                        dest="port",
                        default=Defaults.Port,
                        help="The port number address of the Mate3")
    parser.add_argument("--interval",
                        "-i",
                        dest="interval",
                        default=5,
                        help="Polling interval in seconds",
                        type=int)
    parser.add_argument(
        "--database-url",
        dest="database_url",
        help="Postgres database URL",
        default="postgres://postgres@localhost/postgres",
    )
    parser.add_argument(
        "--definitions",
        dest="definitions",
        default="text",
        help="YAML definition file",
        type=argparse.FileType("r"),
        required=True,
    )
    parser.add_argument(
        "--hypertables",
        dest="hypertables",
        help=
        "Should we create tables as hypertables? Use only if you are using TimescaleDB",
        action="store_true",
    )
    parser.add_argument("--quiet",
                        "-q",
                        dest="quiet",
                        help="Hide status output. Only errors will be shown",
                        action="store_true")
    parser.add_argument("--debug",
                        dest="debug",
                        help="Show debug logging",
                        action="store_true")

    args = parser.parse_args(argv[1:])

    logging.basicConfig(
        format="%(asctime)s - %(levelname)s - %(name)s - %(message)s",
        level=logging.ERROR)
    root_logger = logging.getLogger()
    mate3_logger = logging.getLogger("mate3")

    if args.debug:
        root_logger.setLevel(logging.DEBUG)
    elif args.quiet:
        mate3_logger.setLevel(logging.ERROR)
    else:
        mate3_logger.setLevel(logging.INFO)

    logger.info(f"Connecting to Postgres at {args.database_url}")
    with psycopg2.connect(args.database_url) as conn:
        conn.autocommit = True
        logger.debug("Connected to Postgres")

        # Initial read to get table definitions etc.:
        with Mate3Client(host=args.host, port=args.port) as client:
            devices = client.devices
        tables = read_definitions(args.definitions, client.devices)
        create_tables(conn, tables, hypertables=args.hypertables)

        while True:  # Reconnect loop
            try:
                while True:  # Block fetching loop
                    logger.debug(
                        f"Connecting to mate3 at {args.host}:{args.port}")
                    start = time.time()

                    # Read data from mate3s
                    # We keep the connection open for the minimum time possible
                    # as the mate3s can only sustain one modbus connection at a once.
                    with Mate3Client(host=args.host, port=args.port) as client:
                        devices = client.devices

                    # Insert into postgres
                    insert(conn, tables, devices)

                    # Sleep until the end of this interval
                    total = time.time() - start
                    sleep_time = args.interval - total
                    if sleep_time > 0:
                        time.sleep(args.interval - total)

            except (ModbusIOException, ConnectionException) as e:
                logger.error(
                    f"Communication error: {e}. Will try to reconnect in {args.interval} seconds"
                )
                time.sleep(args.interval)