from __future__ import annotations import sys import typing import logging import argparse import jsonschema import coloredlogs import zigpy_znp.types as t import zigpy_znp.logger as log LOG_LEVELS = [logging.INFO, logging.DEBUG, log._TRACE] OPEN_COORDINATOR_BACKUP_SCHEMA = { "$schema": "http://json-schema.org/draft-07/schema#", "$id": "https://github.com/zigpy/open-coordinator-backup/schema.json", "type": "object", "properties": { "metadata": { "type": "object", "properties": { "format": { "type": "string", "pattern": "^zigpy/open-coordinator-backup$", }, "version": {"type": "integer", "minimum": 1, "maximum": 1}, "source": {"type": "string", "pattern": "^(.*?)+@(.*?)$"}, "internal": {"type": "object"}, }, "required": ["version", "source"], }, "stack_specific": { "type": "object", "properties": { "zstack": { "type": "object", "properties": { "tclk_seed": {"type": "string", "pattern": "[a-fA-F0-9]{32}"}, }, } }, }, "coordinator_ieee": {"type": "string", "pattern": "[a-fA-F0-9]{16}"}, "pan_id": {"type": "string", "pattern": "[a-fA-F0-9]{4}"}, "extended_pan_id": {"type": "string", "pattern": "[a-fA-F0-9]{16}"}, "nwk_update_id": {"type": "integer", "minimum": 0, "maximum": 255}, "security_level": {"type": "integer", "minimum": 0, "maximum": 7}, "channel": {"type": "integer", "minimum": 11, "maximum": 26}, "channel_mask": { "type": "array", "items": {"type": "integer", "minimum": 11, "maximum": 26}, }, "network_key": { "type": "object", "properties": { "key": {"type": "string", "pattern": "[a-fA-F0-9]{32}"}, "sequence_number": {"type": "integer", "minimum": 0, "maximum": 255}, "frame_counter": { "type": "integer", "minimum": 0, "maximum": 4294967295, }, }, "required": ["key", "sequence_number", "frame_counter"], }, "devices": { "type": "array", "items": { "type": "object", "properties": { "nwk_address": { "type": ["string", "null"], "pattern": "[a-fA-F0-9]{4}", }, "ieee_address": {"type": "string", "pattern": "[a-fA-F0-9]{16}"}, "is_child": {"type": "boolean"}, "link_key": { "type": "object", "properties": { "key": {"type": "string", "pattern": "[a-fA-F0-9]{16}"}, "tx_counter": { "type": "integer", "minimum": 0, "maximum": 4294967295, }, "rx_counter": { "type": "integer", "minimum": 0, "maximum": 4294967295, }, }, "required": ["key", "tx_counter", "rx_counter"], }, }, "required": ["nwk_address", "ieee_address"], }, }, }, "required": [ "metadata", "coordinator_ieee", "pan_id", "extended_pan_id", "nwk_update_id", "security_level", "channel", "channel_mask", "network_key", "devices", ], } def validate_backup_json(backup: t.JSONType) -> None: jsonschema.validate(backup, schema=OPEN_COORDINATOR_BACKUP_SCHEMA) class CustomArgumentParser(argparse.ArgumentParser): def parse_args(self, args: typing.Sequence[str] | None = None, namespace=None): args = super().parse_args(args, namespace) # Since we're running as a CLI tool, install our own log level and color logger log.TRACE = log._TRACE logging.addLevelName(log.TRACE, "TRACE") # But still allow the user to configure verbosity verbosity = args.verbosity log_level = LOG_LEVELS[min(max(0, verbosity), len(LOG_LEVELS) - 1)] # coloredlogs uses "spam" for level 5, not "trace" level_styles = coloredlogs.DEFAULT_LEVEL_STYLES.copy() level_styles["trace"] = level_styles["spam"] logging.getLogger().setLevel(log_level) coloredlogs.install( fmt=( "%(asctime)s.%(msecs)03d" " %(hostname)s" " %(name)s" " %(levelname)s %(message)s" ), level=log_level, level_styles=level_styles, ) return args class UnclosableFile: """ Wraps a file object so that every operation but "close" is proxied. """ def __init__(self, f): self.f = f def close(self): return def __enter__(self): return self def __exit__(self, exc_type, exc_value, traceback): return def __getattr__(self, name): return getattr(self.f, name) class ClosableFileType(argparse.FileType): """ Allows `FileType` to always be closed properly, even with stdout and stdin. """ def __call__(self, string): f = super().__call__(string) if f not in (sys.stdin, sys.stdout, sys.stdin.buffer, sys.stdout.buffer): return f return UnclosableFile(f) def setup_parser(description: str) -> argparse.ArgumentParser: """ Creates an ArgumentParser that sets up a logger with a configurable verbosity and a positional serial port argument. """ parser = CustomArgumentParser( description=description, formatter_class=argparse.ArgumentDefaultsHelpFormatter, ) parser.add_argument( "-v", "--verbose", dest="verbosity", action="count", default=0, help="increases verbosity", ) parser.add_argument("serial", type=str, help="Serial port path") return parser