nwp500-python 9.0.0__tar.gz → 9.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/CHANGELOG.rst +109 -0
- {nwp500_python-9.0.0/src/nwp500_python.egg-info → nwp500_python-9.2.0}/PKG-INFO +1 -1
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/docs/reference/python_api/events.rst +17 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/scripts/bump_version.py +86 -16
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/src/nwp500/__init__.py +2 -0
- nwp500_python-9.2.0/src/nwp500/cli/output_formatters.py +172 -0
- nwp500_python-9.0.0/src/nwp500/cli/output_formatters.py → nwp500_python-9.2.0/src/nwp500/cli/presentation.py +189 -358
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/src/nwp500/cli/rich_output.py +144 -402
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/src/nwp500/events.py +20 -3
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/src/nwp500/models/feature.py +20 -2
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/src/nwp500/models/schedule.py +20 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/src/nwp500/models/status.py +20 -2
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/src/nwp500/models/tou.py +8 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/src/nwp500/mqtt/__init__.py +3 -0
- nwp500_python-9.2.0/src/nwp500/mqtt/_control_commands.py +217 -0
- nwp500_python-9.2.0/src/nwp500/mqtt/_device_subscriptions.py +227 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/src/nwp500/mqtt/client.py +22 -401
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/src/nwp500/mqtt/command_queue.py +2 -5
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/src/nwp500/mqtt/connection.py +49 -9
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/src/nwp500/mqtt/state_tracker.py +8 -7
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/src/nwp500/mqtt/subscriptions.py +80 -11
- nwp500_python-9.2.0/src/nwp500/mqtt/types.py +50 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/src/nwp500/mqtt/utils.py +2 -3
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/src/nwp500/mqtt_events.py +14 -2
- {nwp500_python-9.0.0 → nwp500_python-9.2.0/src/nwp500_python.egg-info}/PKG-INFO +1 -1
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/src/nwp500_python.egg-info/SOURCES.txt +6 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/src/nwp500_python.egg-info/scm_file_list.json +6 -0
- nwp500_python-9.2.0/src/nwp500_python.egg-info/scm_version.json +8 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/tests/test_command_queue.py +3 -4
- nwp500_python-9.2.0/tests/test_mqtt_events.py +120 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/tests/test_mqtt_reliability.py +86 -2
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/tests/test_protocol_correctness.py +7 -9
- nwp500_python-9.2.0/tests/test_unit_system_process_wide.py +133 -0
- nwp500_python-9.0.0/src/nwp500_python.egg-info/scm_version.json +0 -8
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/.coveragerc +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/.github/RESOLVING_PR_COMMENTS.md +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/.github/copilot-instructions.md +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/.github/workflows/ci.yml +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/.github/workflows/release.yml +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/.gitignore +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/.pre-commit-config.yaml +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/.readthedocs.yml +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/AUTHORS.rst +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/CONTRIBUTING.rst +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/LICENSE.txt +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/Makefile +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/README.rst +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/RELEASE.md +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/docs/Makefile +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/docs/_static/.gitignore +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/docs/conf.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/docs/explanation/advanced-features.rst +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/docs/explanation/architecture.rst +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/docs/explanation/index.rst +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/docs/how-to/authenticate.rst +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/docs/how-to/auto-recovery.rst +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/docs/how-to/diagnose-mqtt.rst +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/docs/how-to/home-assistant.rst +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/docs/how-to/index.rst +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/docs/how-to/maintenance.rst +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/docs/how-to/manage-units.rst +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/docs/how-to/monitor-status.rst +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/docs/how-to/optimize-tou.rst +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/docs/how-to/queue-commands.rst +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/docs/how-to/schedule-operation.rst +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/docs/how-to/track-energy.rst +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/docs/index.rst +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/docs/openapi.yaml +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/docs/project/authors.rst +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/docs/project/changelog.rst +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/docs/project/contributing.rst +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/docs/project/history.rst +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/docs/project/license.rst +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/docs/reference/configuration.rst +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/docs/reference/enumerations.rst +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/docs/reference/index.rst +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/docs/reference/installation.rst +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/docs/reference/protocol/data_conversions.rst +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/docs/reference/protocol/device_features.rst +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/docs/reference/protocol/device_status.rst +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/docs/reference/protocol/error_codes.rst +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/docs/reference/protocol/mqtt_protocol.rst +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/docs/reference/protocol/quick_reference.rst +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/docs/reference/protocol/rest_api.rst +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/docs/reference/python_api/api_client.rst +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/docs/reference/python_api/auth_client.rst +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/docs/reference/python_api/cli.rst +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/docs/reference/python_api/exceptions.rst +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/docs/reference/python_api/models.rst +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/docs/reference/python_api/mqtt_client.rst +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/docs/requirements.txt +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/docs/tutorials/getting-started.rst +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/examples/.ruff.toml +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/examples/README.md +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/examples/advanced/air_filter_reset.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/examples/advanced/anti_legionella.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/examples/advanced/auto_recovery.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/examples/advanced/combined_callbacks.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/examples/advanced/demand_response.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/examples/advanced/device_capabilities.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/examples/advanced/device_status_debug.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/examples/advanced/energy_analytics.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/examples/advanced/error_code_demo.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/examples/advanced/firmware_payload_capture.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/examples/advanced/mqtt_diagnostics.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/examples/advanced/power_control.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/examples/advanced/recirculation_control.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/examples/advanced/reconnection_demo.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/examples/advanced/reservation_schedule.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/examples/advanced/simple_auto_recovery.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/examples/advanced/token_restoration.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/examples/advanced/tou_openei.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/examples/advanced/tou_schedule.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/examples/advanced/water_reservation.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/examples/beginner/01_authentication.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/examples/beginner/02_list_devices.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/examples/beginner/03_get_status.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/examples/beginner/04_set_temperature.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/examples/intermediate/advanced_auth_patterns.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/examples/intermediate/command_queue.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/examples/intermediate/device_status_callback.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/examples/intermediate/error_handling.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/examples/intermediate/event_driven_control.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/examples/intermediate/improved_auth.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/examples/intermediate/legacy_auth_constructor.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/examples/intermediate/mqtt_realtime_monitoring.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/examples/intermediate/periodic_requests.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/examples/intermediate/set_mode.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/examples/intermediate/vacation_mode.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/examples/mask.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/examples/testing/periodic_device_info.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/examples/testing/simple_periodic_info.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/examples/testing/test_api_client.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/examples/testing/test_mqtt_connection.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/examples/testing/test_mqtt_messaging.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/examples/testing/test_periodic_minimal.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/pyproject.toml +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/scripts/README.md +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/scripts/diagnose_mqtt_connection.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/scripts/extract_changelog.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/scripts/format.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/scripts/lint.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/scripts/setup-dev.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/scripts/validate_version.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/setup.cfg +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/setup.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/src/nwp500/_base.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/src/nwp500/api_client.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/src/nwp500/auth.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/src/nwp500/cli/__init__.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/src/nwp500/cli/__main__.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/src/nwp500/cli/handlers.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/src/nwp500/cli/monitoring.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/src/nwp500/cli/token_storage.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/src/nwp500/command_decorators.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/src/nwp500/config.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/src/nwp500/converters.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/src/nwp500/device_capabilities.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/src/nwp500/device_info_cache.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/src/nwp500/encoding.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/src/nwp500/enums.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/src/nwp500/exceptions.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/src/nwp500/factory.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/src/nwp500/field_factory.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/src/nwp500/models/__init__.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/src/nwp500/models/_converters.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/src/nwp500/models/device.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/src/nwp500/models/energy.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/src/nwp500/models/mqtt_models.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/src/nwp500/mqtt/control.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/src/nwp500/mqtt/diagnostics.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/src/nwp500/mqtt/periodic.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/src/nwp500/mqtt/reconnection.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/src/nwp500/openei.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/src/nwp500/py.typed +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/src/nwp500/reservations.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/src/nwp500/temperature.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/src/nwp500/topic_builder.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/src/nwp500/unit_system.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/src/nwp500/utils.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/src/nwp500_python.egg-info/dependency_links.txt +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/src/nwp500_python.egg-info/entry_points.txt +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/src/nwp500_python.egg-info/not-zip-safe +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/src/nwp500_python.egg-info/requires.txt +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/src/nwp500_python.egg-info/top_level.txt +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/tests/conftest.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/tests/test_api_helpers.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/tests/test_auth.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/tests/test_auth_session_lifecycle.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/tests/test_bug_fixes.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/tests/test_cli_basic.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/tests/test_cli_commands.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/tests/test_command_decorators.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/tests/test_device_capabilities.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/tests/test_device_info_cache.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/tests/test_events.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/tests/test_exceptions.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/tests/test_model_converters.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/tests/test_models.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/tests/test_mqtt_clean_session_resume.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/tests/test_mqtt_client_init.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/tests/test_mqtt_hypothesis.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/tests/test_mqtt_reconnection.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/tests/test_mqtt_reconnection_storm.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/tests/test_multi_device.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/tests/test_openei.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/tests/test_public_api.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/tests/test_reservations.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/tests/test_temperature_converters.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/tests/test_threading_model.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/tests/test_token_storage.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/tests/test_tou_api.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/tests/test_unit_switching.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/tests/test_utility_modules.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/tests/test_utils.py +0 -0
- {nwp500_python-9.0.0 → nwp500_python-9.2.0}/tox.ini +0 -0
|
@@ -5,6 +5,115 @@ Changelog
|
|
|
5
5
|
Unreleased
|
|
6
6
|
==========
|
|
7
7
|
|
|
8
|
+
Version 9.2.0 (2026-07-06)
|
|
9
|
+
==========================
|
|
10
|
+
|
|
11
|
+
Changed
|
|
12
|
+
-------
|
|
13
|
+
- **CLI formatting stacks merged; Rich is the sole human renderer**
|
|
14
|
+
(`#100 <https://github.com/eman/nwp500-python/issues/100>`_): a new
|
|
15
|
+
``src/nwp500/cli/presentation.py`` owns all data-shaping for CLI output
|
|
16
|
+
(field selection, labels, units, ordering, value formatting and energy
|
|
17
|
+
aggregation) as presentation-neutral structures. Because the CLI
|
|
18
|
+
hard-requires ``rich`` (there is no plain-text fallback), the redundant
|
|
19
|
+
plain-text human renderer and the Rich-vs-plain fallback machinery were
|
|
20
|
+
removed: ``rich_output.py`` no longer contains ``_should_use_rich``,
|
|
21
|
+
``_rich_available``, the ``NWP500_NO_RICH`` toggle, or any
|
|
22
|
+
``_print_*_plain`` methods, and now renders the neutral structures
|
|
23
|
+
(including the energy ``TOTAL SUMMARY``) exclusively with Rich, consuming
|
|
24
|
+
the presentation dataclasses directly instead of dict adapters.
|
|
25
|
+
``output_formatters.py`` keeps only JSON/CSV rendering plus thin
|
|
26
|
+
human-output dispatch. Net CLI code drops from ~2088 to ~1838 lines with a
|
|
27
|
+
single data-shaping layer and a single human renderer. Non-energy output
|
|
28
|
+
is byte-for-byte unchanged (verified by golden capture and the existing
|
|
29
|
+
CLI tests); energy output now renders a summary table plus a breakdown
|
|
30
|
+
table entirely in Rich rather than printing plain text and a Rich table.
|
|
31
|
+
- **mqtt/client.py slimmed toward a thin façade** (`#99
|
|
32
|
+
<https://github.com/eman/nwp500-python/issues/99>`_): the ~40 device
|
|
33
|
+
control command proxies and the typed ``subscribe_*``/``unsubscribe_*``
|
|
34
|
+
device-subscription proxies were moved out of ``NavienMqttClient`` into
|
|
35
|
+
two focused mixins, ``DeviceControlCommandsMixin``
|
|
36
|
+
(``mqtt/_control_commands.py``) and ``DeviceSubscriptionsMixin``
|
|
37
|
+
(``mqtt/_device_subscriptions.py``). ``NavienMqttClient`` now inherits
|
|
38
|
+
both, so its public API is unchanged, while ``mqtt/client.py`` shrinks
|
|
39
|
+
from ~1572 to ~1196 lines and reads more clearly as connection
|
|
40
|
+
orchestration plus a public façade. No behavior change.
|
|
41
|
+
- **Event system relationship clarified and de-duplicated** (`#102
|
|
42
|
+
<https://github.com/eman/nwp500-python/issues/102>`_): ``events.py`` and
|
|
43
|
+
``mqtt_events.py`` are not two competing event mechanisms.
|
|
44
|
+
``EventEmitter`` (``events.py``) is the sole delivery mechanism, while
|
|
45
|
+
``mqtt_events.py`` provides the event-name registry
|
|
46
|
+
(``MqttClientEvents``) and the typed dataclass payloads it carries.
|
|
47
|
+
Internal ``emit`` call sites in ``mqtt/state_tracker.py``,
|
|
48
|
+
``mqtt/client.py`` and ``mqtt/subscriptions.py`` now reference the
|
|
49
|
+
``MqttClientEvents`` constants instead of duplicating the raw event-name
|
|
50
|
+
strings, so each event name is defined in exactly one place. Module
|
|
51
|
+
docstrings and the event-system reference docs were updated to document
|
|
52
|
+
the relationship, and typed-payload delivery is now covered by tests.
|
|
53
|
+
- **awscrt types wrapped out of public MQTT signatures** (`#101
|
|
54
|
+
<https://github.com/eman/nwp500-python/issues/101>`_): the library no
|
|
55
|
+
longer exposes ``awscrt`` SDK types on its own public/semi-public API
|
|
56
|
+
surface. A library-owned ``nwp500.mqtt.QoS`` ``IntEnum`` (also exported
|
|
57
|
+
as ``nwp500.QoS``) now replaces ``awscrt.mqtt.QoS`` on the ``publish``
|
|
58
|
+
and ``subscribe`` methods of ``NavienMqttClient``, ``MqttConnection``
|
|
59
|
+
and ``MqttSubscriptionManager``, on ``MqttCommandQueue.enqueue`` and on
|
|
60
|
+
the ``QueuedCommand`` dataclass. ``awscrt.mqtt.Connection`` handles are
|
|
61
|
+
typed behind the ``MqttConnectionHandle`` alias, and translation to/from
|
|
62
|
+
``awscrt`` happens only at the connection-layer boundary
|
|
63
|
+
(``nwp500/mqtt/types.py``). ``NavienMqttClient.publish`` now wraps
|
|
64
|
+
otherwise-uncaught ``AwsCrtError`` in ``MqttPublishError`` so ``awscrt``
|
|
65
|
+
exceptions no longer leak out of the public boundary.
|
|
66
|
+
|
|
67
|
+
.. code-block:: python
|
|
68
|
+
|
|
69
|
+
# OLD
|
|
70
|
+
from awscrt import mqtt
|
|
71
|
+
await client.publish(topic, payload, qos=mqtt.QoS.AT_LEAST_ONCE)
|
|
72
|
+
|
|
73
|
+
# NEW
|
|
74
|
+
from nwp500 import QoS
|
|
75
|
+
await client.publish(topic, payload, qos=QoS.AT_LEAST_ONCE)
|
|
76
|
+
|
|
77
|
+
Fixed
|
|
78
|
+
-----
|
|
79
|
+
- **MQTT ack futures now consumed on abandonment** (`#97
|
|
80
|
+
<https://github.com/eman/nwp500-python/issues/97>`_): ``_await_ack()``
|
|
81
|
+
in ``mqtt/connection.py`` and the inline subscribe/unsubscribe
|
|
82
|
+
acknowledgement waits in ``mqtt/subscriptions.py`` shield the AWS CRT
|
|
83
|
+
future so a timeout or cancellation doesn't propagate into the SDK
|
|
84
|
+
future. Previously, if the shielded future later completed with an
|
|
85
|
+
exception (e.g. ``AwsCrtError`` from clean-session cancellation during
|
|
86
|
+
reconnect) after the awaiting task had already given up, nobody
|
|
87
|
+
retrieved that exception, and asyncio logged a "Future exception was
|
|
88
|
+
never retrieved" warning at garbage-collection time. A done callback
|
|
89
|
+
is now attached whenever a wait is abandoned so the eventual
|
|
90
|
+
result/exception is always retrieved and logged at debug level
|
|
91
|
+
instead of leaking as an unhandled asyncio warning.
|
|
92
|
+
|
|
93
|
+
Documentation
|
|
94
|
+
-------------
|
|
95
|
+
- **Unit-system preference is a deliberate process-wide global** (`#103
|
|
96
|
+
<https://github.com/eman/nwp500-python/issues/103>`_): clarified and
|
|
97
|
+
locked in that the preference in ``nwp500.unit_system`` is an intentional
|
|
98
|
+
process-wide module-level global rather than a
|
|
99
|
+
``contextvars.ContextVar``. A context-local value silently reverted to
|
|
100
|
+
auto-detect for real-time data because MQTT message handling runs in tasks
|
|
101
|
+
scheduled from AWS CRT callback threads whose context never inherits from
|
|
102
|
+
the application task. Added prominent docstring notes to the affected
|
|
103
|
+
models (``DeviceStatus``, ``DeviceFeature``, ``ReservationEntry``,
|
|
104
|
+
``WeeklyReservationEntry``, and ``TOUPeriod``) explaining that unit-aware
|
|
105
|
+
computed fields read the preference at access time and share it across all
|
|
106
|
+
async tasks and threads. This is a non-breaking documentation and test
|
|
107
|
+
change; no API changes.
|
|
108
|
+
|
|
109
|
+
Added
|
|
110
|
+
-----
|
|
111
|
+
- **Tests for process-wide unit-system semantics** (`#103
|
|
112
|
+
<https://github.com/eman/nwp500-python/issues/103>`_): new
|
|
113
|
+
``tests/test_unit_system_process_wide.py`` verifies that setting the global
|
|
114
|
+
preference affects already-constructed model instances at access time and
|
|
115
|
+
that the preference is visible across async tasks and threads.
|
|
116
|
+
|
|
8
117
|
Version 9.0.0 (2026-07-05)
|
|
9
118
|
==========================
|
|
10
119
|
|
|
@@ -17,6 +17,23 @@ Use the event system when you want to react to connection changes, status
|
|
|
17
17
|
transitions, or derived state changes such as temperature deltas and error
|
|
18
18
|
conditions.
|
|
19
19
|
|
|
20
|
+
How the pieces fit together
|
|
21
|
+
---------------------------
|
|
22
|
+
|
|
23
|
+
The event system is split across two complementary modules:
|
|
24
|
+
|
|
25
|
+
* :mod:`nwp500.events` provides the delivery **mechanism** — the generic
|
|
26
|
+
:class:`~nwp500.events.EventEmitter` (multiple listeners, async handlers,
|
|
27
|
+
one-time listeners, priority ordering). ``NavienMqttClient`` extends it.
|
|
28
|
+
* :mod:`nwp500.mqtt_events` provides the **vocabulary** — the
|
|
29
|
+
:class:`~nwp500.mqtt_events.MqttClientEvents` name registry and the typed,
|
|
30
|
+
frozen dataclass payloads carried by each event.
|
|
31
|
+
|
|
32
|
+
These are not two competing systems. Internal ``emit`` call sites reference the
|
|
33
|
+
``MqttClientEvents`` constants and emit the matching payload dataclass, and you
|
|
34
|
+
subscribe with the same constants, so an event name is defined in exactly one
|
|
35
|
+
place.
|
|
36
|
+
|
|
20
37
|
Two Subscription Patterns
|
|
21
38
|
=========================
|
|
22
39
|
|
|
@@ -20,17 +20,20 @@ field is for the PyScaffold tool version, not the package version!
|
|
|
20
20
|
"""
|
|
21
21
|
|
|
22
22
|
import re
|
|
23
|
+
import shutil
|
|
23
24
|
import subprocess
|
|
24
25
|
import sys
|
|
25
|
-
from datetime import
|
|
26
|
+
from datetime import datetime
|
|
26
27
|
from pathlib import Path
|
|
27
28
|
|
|
29
|
+
GIT = shutil.which("git") or "git"
|
|
28
30
|
|
|
29
|
-
|
|
31
|
+
|
|
32
|
+
def run_git_command(args: list[str]) -> str:
|
|
30
33
|
"""Run a git command and return the output."""
|
|
31
34
|
try:
|
|
32
|
-
result = subprocess.run(
|
|
33
|
-
[
|
|
35
|
+
result = subprocess.run( # noqa: S603
|
|
36
|
+
[GIT, *args],
|
|
34
37
|
capture_output=True,
|
|
35
38
|
text=True,
|
|
36
39
|
check=True,
|
|
@@ -142,6 +145,73 @@ def check_working_directory_clean() -> None:
|
|
|
142
145
|
sys.exit(1)
|
|
143
146
|
|
|
144
147
|
|
|
148
|
+
def check_up_to_date_with_remote() -> None:
|
|
149
|
+
"""Ensure the local branch is in sync with its remote before bumping.
|
|
150
|
+
|
|
151
|
+
Running the version bump from a stale local checkout can silently drop
|
|
152
|
+
CHANGELOG.rst entries: the local file is missing changes (e.g. a prior
|
|
153
|
+
release's changelog section) that already exist upstream, so the diff
|
|
154
|
+
inserted by update_changelog() is based on outdated content and the
|
|
155
|
+
upstream entry never makes it back in once pushed/merged.
|
|
156
|
+
"""
|
|
157
|
+
branch = run_git_command(["rev-parse", "--abbrev-ref", "HEAD"])
|
|
158
|
+
upstream = subprocess.run( # noqa: S603
|
|
159
|
+
[GIT, "rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}"],
|
|
160
|
+
capture_output=True,
|
|
161
|
+
text=True,
|
|
162
|
+
)
|
|
163
|
+
if upstream.returncode != 0:
|
|
164
|
+
print(
|
|
165
|
+
f"Warning: Branch '{branch}' has no upstream tracking branch; "
|
|
166
|
+
"skipping remote sync check.",
|
|
167
|
+
file=sys.stderr,
|
|
168
|
+
)
|
|
169
|
+
return
|
|
170
|
+
|
|
171
|
+
upstream_ref = upstream.stdout.strip()
|
|
172
|
+
|
|
173
|
+
# Fetch quietly so the comparison below reflects the latest remote state.
|
|
174
|
+
fetch = subprocess.run( # noqa: S603
|
|
175
|
+
[GIT, "fetch", "--quiet"],
|
|
176
|
+
capture_output=True,
|
|
177
|
+
text=True,
|
|
178
|
+
)
|
|
179
|
+
if fetch.returncode != 0:
|
|
180
|
+
print(
|
|
181
|
+
f"Warning: 'git fetch' failed, remote sync check may be stale: "
|
|
182
|
+
f"{fetch.stderr.strip()}",
|
|
183
|
+
file=sys.stderr,
|
|
184
|
+
)
|
|
185
|
+
|
|
186
|
+
local_sha = run_git_command(["rev-parse", "HEAD"])
|
|
187
|
+
upstream_sha = run_git_command(["rev-parse", upstream_ref])
|
|
188
|
+
|
|
189
|
+
if local_sha == upstream_sha:
|
|
190
|
+
return
|
|
191
|
+
|
|
192
|
+
behind = run_git_command(["rev-list", "--count", f"HEAD..{upstream_ref}"])
|
|
193
|
+
ahead = run_git_command(["rev-list", "--count", f"{upstream_ref}..HEAD"])
|
|
194
|
+
|
|
195
|
+
if int(behind) > 0:
|
|
196
|
+
print(
|
|
197
|
+
f"Error: Local branch '{branch}' is {behind} commit(s) behind "
|
|
198
|
+
f"'{upstream_ref}'.",
|
|
199
|
+
file=sys.stderr,
|
|
200
|
+
)
|
|
201
|
+
print(
|
|
202
|
+
"Pull the latest changes before bumping the version, otherwise "
|
|
203
|
+
"CHANGELOG.rst entries from other releases may be lost.",
|
|
204
|
+
file=sys.stderr,
|
|
205
|
+
)
|
|
206
|
+
sys.exit(1)
|
|
207
|
+
|
|
208
|
+
if int(ahead) > 0:
|
|
209
|
+
print(
|
|
210
|
+
f"Warning: Local branch '{branch}' is {ahead} commit(s) ahead of "
|
|
211
|
+
f"'{upstream_ref}' (unpushed commits)."
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
|
|
145
215
|
def update_changelog(version: str) -> None:
|
|
146
216
|
"""Insert a version heading into CHANGELOG.rst below the Unreleased section.
|
|
147
217
|
|
|
@@ -169,7 +239,8 @@ def update_changelog(version: str) -> None:
|
|
|
169
239
|
|
|
170
240
|
content = changelog_path.read_text(encoding="utf-8")
|
|
171
241
|
|
|
172
|
-
|
|
242
|
+
today = datetime.now().astimezone().date().isoformat()
|
|
243
|
+
heading = f"Version {version} ({today})"
|
|
173
244
|
underline = "=" * len(heading)
|
|
174
245
|
version_block = f"{heading}\n{underline}\n"
|
|
175
246
|
|
|
@@ -177,7 +248,7 @@ def update_changelog(version: str) -> None:
|
|
|
177
248
|
# one or more blank lines, then insert the version block after them.
|
|
178
249
|
pattern = re.compile(
|
|
179
250
|
r"(Unreleased\n=+\n)" # group 1: Unreleased heading
|
|
180
|
-
r"(\n+)",
|
|
251
|
+
r"(\n+)", # group 2: blank line(s) separator
|
|
181
252
|
re.MULTILINE,
|
|
182
253
|
)
|
|
183
254
|
|
|
@@ -192,10 +263,7 @@ def update_changelog(version: str) -> None:
|
|
|
192
263
|
|
|
193
264
|
# Insert the version block after the blank lines that follow "Unreleased"
|
|
194
265
|
new_content = (
|
|
195
|
-
content[: match.end()]
|
|
196
|
-
+ version_block
|
|
197
|
-
+ "\n"
|
|
198
|
-
+ content[match.end() :]
|
|
266
|
+
content[: match.end()] + version_block + "\n" + content[match.end() :]
|
|
199
267
|
)
|
|
200
268
|
|
|
201
269
|
changelog_path.write_text(new_content, encoding="utf-8")
|
|
@@ -205,20 +273,18 @@ def update_changelog(version: str) -> None:
|
|
|
205
273
|
def commit_changelog(version: str) -> None:
|
|
206
274
|
"""Stage and commit the CHANGELOG.rst update."""
|
|
207
275
|
run_git_command(["add", "CHANGELOG.rst"])
|
|
208
|
-
run_git_command(
|
|
209
|
-
["commit", "-m", f"Update changelog for v{version}"]
|
|
210
|
-
)
|
|
276
|
+
run_git_command(["commit", "-m", f"Update changelog for v{version}"])
|
|
211
277
|
print("[OK] Committed changelog update")
|
|
212
278
|
|
|
213
279
|
|
|
214
|
-
def create_tag(version: str, message: str = None) -> None:
|
|
280
|
+
def create_tag(version: str, message: str | None = None) -> None:
|
|
215
281
|
"""Create a git tag for the version."""
|
|
216
282
|
tag_name = f"v{version}"
|
|
217
283
|
|
|
218
284
|
# Check if tag already exists
|
|
219
285
|
try:
|
|
220
|
-
subprocess.run(
|
|
221
|
-
[
|
|
286
|
+
subprocess.run( # noqa: S603
|
|
287
|
+
[GIT, "rev-parse", tag_name],
|
|
222
288
|
capture_output=True,
|
|
223
289
|
check=True,
|
|
224
290
|
)
|
|
@@ -283,6 +349,10 @@ def main() -> None:
|
|
|
283
349
|
# Check working directory is clean
|
|
284
350
|
check_working_directory_clean()
|
|
285
351
|
|
|
352
|
+
# Ensure we're not bumping from a stale checkout, which can silently
|
|
353
|
+
# drop CHANGELOG.rst entries from releases made on the remote branch.
|
|
354
|
+
check_up_to_date_with_remote()
|
|
355
|
+
|
|
286
356
|
# Get current version
|
|
287
357
|
current_version = get_current_version()
|
|
288
358
|
print(f"Current version: {current_version}")
|
|
@@ -110,6 +110,7 @@ from nwp500.mqtt import (
|
|
|
110
110
|
MqttMetrics,
|
|
111
111
|
NavienMqttClient,
|
|
112
112
|
PeriodicRequestType,
|
|
113
|
+
QoS,
|
|
113
114
|
)
|
|
114
115
|
from nwp500.mqtt_events import (
|
|
115
116
|
MqttClientEvents,
|
|
@@ -221,6 +222,7 @@ __all__ = [
|
|
|
221
222
|
"MqttMetrics",
|
|
222
223
|
"ConnectionDropEvent",
|
|
223
224
|
"ConnectionEvent",
|
|
225
|
+
"QoS",
|
|
224
226
|
# Event Emitter
|
|
225
227
|
"EventEmitter",
|
|
226
228
|
"EventListener",
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
"""CSV and JSON renderers plus human-output dispatch for the CLI.
|
|
2
|
+
|
|
3
|
+
Data-shaping (which fields, labels, units, ordering, aggregation) lives in
|
|
4
|
+
:mod:`.presentation`; human-readable rendering is delegated to the Rich
|
|
5
|
+
renderer in :mod:`.rich_output`, which consumes the same neutral structures.
|
|
6
|
+
This module additionally handles the genuinely different CSV and JSON outputs.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
import csv
|
|
10
|
+
import json
|
|
11
|
+
import logging
|
|
12
|
+
from datetime import datetime
|
|
13
|
+
from enum import Enum
|
|
14
|
+
from pathlib import Path
|
|
15
|
+
from typing import Any
|
|
16
|
+
|
|
17
|
+
from nwp500 import DeviceStatus
|
|
18
|
+
|
|
19
|
+
from .presentation import (
|
|
20
|
+
build_daily_energy_report,
|
|
21
|
+
build_device_info_rows,
|
|
22
|
+
build_device_status_rows,
|
|
23
|
+
build_energy_report,
|
|
24
|
+
)
|
|
25
|
+
from .rich_output import get_formatter
|
|
26
|
+
|
|
27
|
+
_logger = logging.getLogger(__name__)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _json_default_serializer(obj: Any) -> Any:
|
|
31
|
+
"""Serialize objects not serializable by default json code.
|
|
32
|
+
|
|
33
|
+
Note: Enums are handled by model.model_dump() which converts them to names.
|
|
34
|
+
This function handles any remaining non-JSON-serializable types that might
|
|
35
|
+
appear in raw MQTT messages.
|
|
36
|
+
|
|
37
|
+
Args:
|
|
38
|
+
obj: Object to serialize
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
JSON-serializable representation of the object
|
|
42
|
+
|
|
43
|
+
Raises:
|
|
44
|
+
TypeError: If object cannot be serialized
|
|
45
|
+
"""
|
|
46
|
+
if isinstance(obj, datetime):
|
|
47
|
+
return obj.isoformat()
|
|
48
|
+
if isinstance(obj, Enum):
|
|
49
|
+
return obj.name # Fallback for any enums not in model output
|
|
50
|
+
# Handle Pydantic models
|
|
51
|
+
if hasattr(obj, "model_dump"):
|
|
52
|
+
return obj.model_dump()
|
|
53
|
+
raise TypeError(f"Type {type(obj)} not serializable")
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def print_energy_usage(energy_response: Any) -> None:
|
|
57
|
+
"""Print energy usage data (summary + monthly breakdown) via Rich.
|
|
58
|
+
|
|
59
|
+
Args:
|
|
60
|
+
energy_response: EnergyUsageResponse object
|
|
61
|
+
"""
|
|
62
|
+
report = build_energy_report(energy_response)
|
|
63
|
+
get_formatter().print_energy_table(report)
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def print_daily_energy_usage(
|
|
67
|
+
energy_response: Any, year: int, month: int
|
|
68
|
+
) -> None:
|
|
69
|
+
"""Print daily energy usage for a specific month via Rich.
|
|
70
|
+
|
|
71
|
+
Args:
|
|
72
|
+
energy_response: EnergyUsageResponse object
|
|
73
|
+
year: Year to filter for (e.g., 2025)
|
|
74
|
+
month: Month to filter for (1-12)
|
|
75
|
+
"""
|
|
76
|
+
report = build_daily_energy_report(energy_response, year, month)
|
|
77
|
+
formatter = get_formatter()
|
|
78
|
+
if report is None:
|
|
79
|
+
formatter.print_info(
|
|
80
|
+
f"No daily energy data available for {month}/{year}"
|
|
81
|
+
)
|
|
82
|
+
return
|
|
83
|
+
formatter.print_daily_energy_table(report)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def write_status_to_csv(file_path: str, status: DeviceStatus) -> None:
|
|
87
|
+
"""
|
|
88
|
+
Append device status to a CSV file.
|
|
89
|
+
|
|
90
|
+
Args:
|
|
91
|
+
file_path: Path to the CSV file
|
|
92
|
+
status: DeviceStatus object to write
|
|
93
|
+
"""
|
|
94
|
+
try:
|
|
95
|
+
# Convert status to dict (enums are already converted to names)
|
|
96
|
+
status_dict = status.model_dump()
|
|
97
|
+
|
|
98
|
+
# Add a timestamp to the beginning of the data (timezone-aware,
|
|
99
|
+
# in the local timezone)
|
|
100
|
+
status_dict["timestamp"] = datetime.now().astimezone().isoformat()
|
|
101
|
+
|
|
102
|
+
# Check if file exists to determine if we need to write the header
|
|
103
|
+
file_exists = Path(file_path).exists()
|
|
104
|
+
|
|
105
|
+
with Path(file_path).open("a", newline="") as csvfile:
|
|
106
|
+
# Get the field names from the dict keys
|
|
107
|
+
fieldnames = list(status_dict.keys())
|
|
108
|
+
writer = csv.DictWriter(csvfile, fieldnames=fieldnames)
|
|
109
|
+
|
|
110
|
+
# Write header only if this is a new file
|
|
111
|
+
if not file_exists:
|
|
112
|
+
writer.writeheader()
|
|
113
|
+
|
|
114
|
+
writer.writerow(status_dict)
|
|
115
|
+
|
|
116
|
+
_logger.debug(f"Status written to {file_path}")
|
|
117
|
+
|
|
118
|
+
except OSError as e:
|
|
119
|
+
_logger.error(f"Failed to write to CSV: {e}")
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def format_json_output(data: Any, indent: int = 2) -> str:
|
|
123
|
+
"""
|
|
124
|
+
Format data as JSON string with custom serialization.
|
|
125
|
+
|
|
126
|
+
Args:
|
|
127
|
+
data: Data to format
|
|
128
|
+
indent: Number of spaces for indentation (default: 2)
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
JSON-formatted string
|
|
132
|
+
"""
|
|
133
|
+
return json.dumps(data, indent=indent, default=_json_default_serializer)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
def print_json(data: Any, indent: int = 2) -> None:
|
|
137
|
+
"""
|
|
138
|
+
Print data as formatted JSON with optional syntax highlighting.
|
|
139
|
+
|
|
140
|
+
Uses Rich highlighting when available, falls back to plain JSON otherwise.
|
|
141
|
+
|
|
142
|
+
Args:
|
|
143
|
+
data: Data to print
|
|
144
|
+
indent: Number of spaces for indentation (default: 2)
|
|
145
|
+
"""
|
|
146
|
+
json_str = format_json_output(data, indent)
|
|
147
|
+
formatter = get_formatter()
|
|
148
|
+
formatter.print_json_highlighted(json.loads(json_str))
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def print_device_status(device_status: Any) -> None:
|
|
152
|
+
"""
|
|
153
|
+
Print device status with aligned columns and dynamic width calculation.
|
|
154
|
+
|
|
155
|
+
Units are automatically extracted from the DeviceStatus model metadata.
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
device_status: DeviceStatus object
|
|
159
|
+
"""
|
|
160
|
+
formatter = get_formatter()
|
|
161
|
+
formatter.print_status_table(build_device_status_rows(device_status))
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
def print_device_info(device_feature: Any) -> None:
|
|
165
|
+
"""
|
|
166
|
+
Print device information with aligned columns and dynamic width calculation.
|
|
167
|
+
|
|
168
|
+
Args:
|
|
169
|
+
device_feature: DeviceFeature object
|
|
170
|
+
"""
|
|
171
|
+
formatter = get_formatter()
|
|
172
|
+
formatter.print_status_table(build_device_info_rows(device_feature))
|