From 66f36c6b981a26da4392f000780a6ce106abc285 Mon Sep 17 00:00:00 2001 From: Thomas Hahn Date: Sun, 9 Jun 2024 13:34:20 +0200 Subject: [PATCH] add listen cli command --- src/homematicip/cli/hmip.py | 68 ++++++++++++------- .../connection/websocket_handler.py | 5 ++ src/homematicip/runner.py | 6 +- tests/cli/test_hmip.py | 15 ++++ 4 files changed, 67 insertions(+), 27 deletions(-) diff --git a/src/homematicip/cli/hmip.py b/src/homematicip/cli/hmip.py index f7a3dccf..86abf3e1 100644 --- a/src/homematicip/cli/hmip.py +++ b/src/homematicip/cli/hmip.py @@ -30,6 +30,7 @@ from homematicip.events.event_types import ModelUpdateEvent from homematicip.model.anoymizer import handle_config from homematicip.model.enums import ClimateControlMode, ClimateControlDisplay +from homematicip.model.hmip_base import HmipBaseModel from homematicip.model.model_components import Device @@ -265,30 +266,42 @@ def firmware(): async def _model_update_event_handler(event: ModelUpdateEvent, args) -> None: - click.echo(f"ModelUpdateEvent: {event}; {args}") + hmip_item = None + if isinstance(args, HmipBaseModel): + hmip_item = args + output = {"event": event.name, + "updated_type": type(args).__name__ if args else None, + "updated_id": getattr(args, "id") if args else None, + "updated_item": repr(hmip_item) if hmip_item else None} + + formatted_output = json.dumps(output, indent=4) + click.echo(formatted_output) + + +@cli.command +def listen(): + """Listen to events. If filename is specified, events are written to the file. If not, they are written to + console.""" + asyncio.run(listen_wrapped()) -# @cli.command -# def listen(): -# """Listen to events. If filename is specified, events are written to the file. If not, they are written to -# console.""" -# runner = asyncio.run(get_initialized_runner()) -# runner.event_manager.subscribe(ModelUpdateEvent.ITEM_CREATED, _model_update_event_handler) -# runner.event_manager.subscribe(ModelUpdateEvent.ITEM_UPDATED, _model_update_event_handler) -# runner.event_manager.subscribe(ModelUpdateEvent.ITEM_REMOVED, _model_update_event_handler) -# -# loop = asyncio.new_event_loop() -# task = loop.create_task(runner.async_listening_for_updates()) -# -# try: -# click.echo("Waiting for events... press CTRL+C to stop listening.") -# loop.run_until_complete(task) -# except asyncio.CancelledError: -# pass -# except KeyboardInterrupt: -# task.cancel() -# loop.stop() -# click.echo("Stopping listener...") + +async def listen_wrapped(): + """The wrapped listen function.""" + runner = await get_initialized_runner() + runner.event_manager.subscribe(ModelUpdateEvent.ITEM_CREATED, _model_update_event_handler) + runner.event_manager.subscribe(ModelUpdateEvent.ITEM_UPDATED, _model_update_event_handler) + runner.event_manager.subscribe(ModelUpdateEvent.ITEM_REMOVED, _model_update_event_handler) + + task = asyncio.create_task(runner.async_listening_for_updates()) + + try: + click.echo("Waiting for events... press CTRL+C to stop listening.") + await task + except KeyboardInterrupt: + click.echo("Stop listener...") + task.cancel() + await task @cli.command @@ -324,7 +337,8 @@ def turn_on(id: str, channel: int = None): f"Run turn_on for device {get_device_name(device_or_group)} with result: {result.status_text} ({result.status})") else: result = asyncio.run(action_set_switch_state_group(runner.rest_connection, device_or_group, True)) - click.echo(f"Run turn_on for device {get_group_name(device_or_group)} with result: {result.status_text} ({result.status})") + click.echo( + f"Run turn_on for device {get_group_name(device_or_group)} with result: {result.status_text} ({result.status})") @run.command() @@ -443,7 +457,8 @@ def set_point_temperature(id: str, temperature: float): click.echo(f"Group with id {id} not found.", err=True, color=True) return - result = asyncio.run(action_set_point_temperature_group(runner.rest_connection, runner.model.groups[id], temperature)) + result = asyncio.run( + action_set_point_temperature_group(runner.rest_connection, runner.model.groups[id], temperature)) if result.exception is not None: click.echo(f"Error while running set_point_temperature: {result.exception}", err=True, color=True) return @@ -483,7 +498,8 @@ def set_active_profile(id: str, profile_index: str): click.echo(f"Group with id {id} not found.", err=True, color=True) return - result = asyncio.run(action_set_active_profile_group(runner.rest_connection, runner.model.groups[id], profile_index)) + result = asyncio.run( + action_set_active_profile_group(runner.rest_connection, runner.model.groups[id], profile_index)) click.echo( f"Run set_active_profile for group {runner.model.groups[id].label or runner.model.groups[id].id} with " f"result: {result.status_text} ({result.status})") @@ -527,6 +543,7 @@ def set_display(id: str, channel: int, display: str): f"Run set_display for group {get_device_name(device)} with " f"result: {result.status_text} ({result.status})") + @run.command @click.option("--id", type=str, required=True, help="ID of the device or group, which the run command is applied to.") @click.option("-d", "--dim_level", type=click.FloatRange(0.0, 1.0), help="Target Dim Level", required=True) @@ -659,6 +676,7 @@ def set_on_time(id: str, on_time: int): click.echo( f"Run set_on_time for group {get_group_name(group)} with result: {result.status_text} ({result.status})") + @run.command @click.option("--id", type=str, required=True, help="ID of the device or group, which the run command is applied to.") @click.option("-c", "--channel", type=int, required=False, default=None, diff --git a/src/homematicip/connection/websocket_handler.py b/src/homematicip/connection/websocket_handler.py index e0689cb7..0d36a7d4 100644 --- a/src/homematicip/connection/websocket_handler.py +++ b/src/homematicip/connection/websocket_handler.py @@ -1,5 +1,7 @@ +import ssl from typing import Callable +import certifi import websockets from homematicip.connection.rest_connection import ConnectionContext, ATTR_AUTH_TOKEN, ATTR_CLIENT_AUTH, LOGGER @@ -9,12 +11,15 @@ class WebSocketHandler: async def listen(self, context: ConnectionContext, connection_state_callback: Callable, reconnect_on_error: bool = True): uri = context.websocket_url + ssl_context = ssl.create_default_context() + ssl_context.load_verify_locations(certifi.where()) async with websockets.connect( uri, extra_headers={ ATTR_AUTH_TOKEN: context.auth_token, ATTR_CLIENT_AUTH: context.client_auth_token, }, + ssl=ssl_context, ) as websocket: # Process messages received on the connection. async for message in websocket: diff --git a/src/homematicip/runner.py b/src/homematicip/runner.py index 610d6d6f..d8dc875f 100644 --- a/src/homematicip/runner.py +++ b/src/homematicip/runner.py @@ -58,7 +58,6 @@ def rest_connection(self): pass - @dataclass(kw_only=True) class Runner(AbstractRunner): model: Model = None @@ -110,7 +109,10 @@ async def async_initialize_runner_without_init_model(self): async def async_listening_for_updates(self): """Start listening for updates from HomematicIP Cloud. This method will not return.""" - await self._async_start_listening_for_updates(self._connection_context) + try: + await self._async_start_listening_for_updates(self._connection_context) + except asyncio.CancelledError: + pass async def async_get_current_state(self): """ diff --git a/tests/cli/test_hmip.py b/tests/cli/test_hmip.py index 95f7da02..2ce9a025 100644 --- a/tests/cli/test_hmip.py +++ b/tests/cli/test_hmip.py @@ -1,6 +1,15 @@ +import pytest from click.testing import CliRunner from homematicip.cli import hmip +from homematicip.cli.hmip import _model_update_event_handler +from homematicip.events.event_types import ModelUpdateEvent +from homematicip.model.model import Model, build_model_from_json + + +@pytest.fixture +def filled_model(sample_data_complete) -> Model: + return build_model_from_json(sample_data_complete) # # def test_version(): @@ -11,3 +20,9 @@ # def test_list_devices(mocker): # result = CliRunner().invoke(hmip.cli, ["list", "devices"]) # assert result.exit_code == 0 + +@pytest.mark.asyncio +async def test_model_update_event_handler(filled_model: Model): + device = filled_model.devices["3014F7110000RAIN_SENSOR"] + + await _model_update_event_handler(ModelUpdateEvent.ITEM_UPDATED, device) \ No newline at end of file