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." )
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)
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)
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}"
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)