|
| 1 | +#!/usr/bin/env python3 |
| 2 | +"""Remote entity integration example. Bare minimum of an integration driver.""" |
| 3 | +import asyncio |
| 4 | +import json |
| 5 | +import logging |
| 6 | +import sys |
| 7 | +from typing import Any |
| 8 | + |
| 9 | +import ucapi |
| 10 | +from ucapi import remote |
| 11 | +from ucapi.remote import * |
| 12 | +from ucapi.remote import create_send_cmd, create_sequence_cmd |
| 13 | +from ucapi.ui import ( |
| 14 | + Buttons, |
| 15 | + DeviceButtonMapping, |
| 16 | + Size, |
| 17 | + UiPage, |
| 18 | + create_btn_mapping, |
| 19 | + create_ui_icon, |
| 20 | + create_ui_text, |
| 21 | +) |
| 22 | + |
| 23 | +loop = asyncio.get_event_loop() |
| 24 | +api = ucapi.IntegrationAPI(loop) |
| 25 | + |
| 26 | +# Simple commands which are supported by this example remote-entity |
| 27 | +supported_commands = [ |
| 28 | + "VOLUME_UP", |
| 29 | + "VOLUME_DOWN", |
| 30 | + "HOME", |
| 31 | + "GUIDE", |
| 32 | + "CONTEXT_MENU", |
| 33 | + "CURSOR_UP", |
| 34 | + "CURSOR_DOWN", |
| 35 | + "CURSOR_LEFT", |
| 36 | + "CURSOR_RIGHT", |
| 37 | + "CURSOR_ENTER", |
| 38 | + "MY_RECORDINGS", |
| 39 | + "MY_APPS", |
| 40 | + "REVERSE", |
| 41 | + "PLAY", |
| 42 | + "PAUSE", |
| 43 | + "FORWARD", |
| 44 | + "RECORD", |
| 45 | +] |
| 46 | + |
| 47 | + |
| 48 | +async def cmd_handler( |
| 49 | + entity: ucapi.Remote, cmd_id: str, params: dict[str, Any] | None |
| 50 | +) -> ucapi.StatusCodes: |
| 51 | + """ |
| 52 | + Remote command handler. |
| 53 | +
|
| 54 | + Called by the integration-API if a command is sent to a configured remote-entity. |
| 55 | +
|
| 56 | + :param entity: remote entity |
| 57 | + :param cmd_id: command |
| 58 | + :param params: optional command parameters |
| 59 | + :return: status of the command |
| 60 | + """ |
| 61 | + print(f"Got {entity.id} command request: {cmd_id}") |
| 62 | + |
| 63 | + state = None |
| 64 | + match cmd_id: |
| 65 | + case remote.Commands.ON: |
| 66 | + state = remote.States.ON |
| 67 | + case remote.Commands.OFF: |
| 68 | + state = remote.States.OFF |
| 69 | + case remote.Commands.TOGGLE: |
| 70 | + if entity.attributes[remote.Attributes.STATE] == remote.States.OFF: |
| 71 | + state = remote.States.ON |
| 72 | + else: |
| 73 | + state = remote.States.OFF |
| 74 | + case remote.Commands.SEND_CMD: |
| 75 | + command = params.get("command") |
| 76 | + # It's up to the integration what to do with an unknown command. |
| 77 | + # If the supported commands are provided as simple_commands, then it's |
| 78 | + # easy to validate. |
| 79 | + if command not in supported_commands: |
| 80 | + print(f"Unknown command: {command}", file=sys.stderr) |
| 81 | + return ucapi.StatusCodes.BAD_REQUEST |
| 82 | + |
| 83 | + repeat = params.get("repeat", 1) |
| 84 | + delay = params.get("delay", 0) |
| 85 | + hold = params.get("hold", 0) |
| 86 | + print(f"Command: {command} (repeat={repeat}, delay={delay}, hold={hold})") |
| 87 | + case remote.Commands.SEND_CMD_SEQUENCE: |
| 88 | + sequence = params.get("sequence") |
| 89 | + repeat = params.get("repeat", 1) |
| 90 | + delay = params.get("delay", 0) |
| 91 | + hold = params.get("hold", 0) |
| 92 | + print( |
| 93 | + f"Command sequence: {sequence} (repeat={repeat}, delay={delay}, hold={hold})" |
| 94 | + ) |
| 95 | + case _: |
| 96 | + print(f"Unsupported command: {cmd_id}", file=sys.stderr) |
| 97 | + return ucapi.StatusCodes.BAD_REQUEST |
| 98 | + |
| 99 | + if state: |
| 100 | + api.configured_entities.update_attributes( |
| 101 | + entity.id, {remote.Attributes.STATE: state} |
| 102 | + ) |
| 103 | + |
| 104 | + return ucapi.StatusCodes.OK |
| 105 | + |
| 106 | + |
| 107 | +@api.listens_to(ucapi.Events.CONNECT) |
| 108 | +async def on_connect() -> None: |
| 109 | + """When the UCR2 connects, send the device state.""" |
| 110 | + # This example is ready all the time! |
| 111 | + await api.set_device_state(ucapi.DeviceStates.CONNECTED) |
| 112 | + |
| 113 | + |
| 114 | +def create_button_mappings() -> list[DeviceButtonMapping | dict[str, Any]]: |
| 115 | + """Create a demo button mapping showing different composition options.""" |
| 116 | + return [ |
| 117 | + # simple short- and long-press mapping |
| 118 | + create_btn_mapping(Buttons.HOME, "HOME", "GUIDE"), |
| 119 | + # use channel buttons for volume control |
| 120 | + create_btn_mapping(Buttons.CHANNEL_DOWN, "VOLUME_DOWN"), |
| 121 | + create_btn_mapping(Buttons.CHANNEL_UP, "VOLUME_UP"), |
| 122 | + create_btn_mapping(Buttons.DPAD_UP, "CURSOR_UP"), |
| 123 | + create_btn_mapping(Buttons.DPAD_DOWN, "CURSOR_DOWN"), |
| 124 | + create_btn_mapping(Buttons.DPAD_LEFT, "CURSOR_LEFT"), |
| 125 | + create_btn_mapping(Buttons.DPAD_RIGHT, "CURSOR_RIGHT"), |
| 126 | + # use a send command |
| 127 | + create_btn_mapping( |
| 128 | + Buttons.DPAD_MIDDLE, create_send_cmd("CONTEXT_MENU", hold=1000) |
| 129 | + ), |
| 130 | + # use a sequence command |
| 131 | + create_btn_mapping( |
| 132 | + Buttons.BLUE, |
| 133 | + create_sequence_cmd( |
| 134 | + [ |
| 135 | + "CURSOR_UP", |
| 136 | + "CURSOR_RIGHT", |
| 137 | + "CURSOR_DOWN", |
| 138 | + "CURSOR_LEFT", |
| 139 | + ], |
| 140 | + delay=200, |
| 141 | + ), |
| 142 | + ), |
| 143 | + # Safety off: don't use a DeviceButtonMapping data class but a dictionary. |
| 144 | + # This is useful for directly reading a json configuration file. |
| 145 | + {"button": "POWER", "short_press": {"cmd_id": "remote.toggle"}}, |
| 146 | + ] |
| 147 | + |
| 148 | + |
| 149 | +def create_ui() -> list[UiPage | dict[str, Any]]: |
| 150 | + """Create a demo user interface showing different composition options.""" |
| 151 | + # Safety off again: directly use json structure to read a configuration file |
| 152 | + with open("remote_ui_page.json", "r", encoding="utf-8") as file: |
| 153 | + main_page = json.load(file) |
| 154 | + |
| 155 | + # On-the-fly UI composition |
| 156 | + ui_page1 = UiPage("page1", "Main") |
| 157 | + ui_page1.add(create_ui_text("Hello remote entity", 0, 0, size=Size(4, 1))) |
| 158 | + ui_page1.add(create_ui_icon("uc:home", 0, 2, cmd="HOME")) |
| 159 | + ui_page1.add(create_ui_icon("uc:up-arrow-bold", 2, 2, cmd="CURSOR_UP")) |
| 160 | + ui_page1.add(create_ui_icon("uc:down-arrow-bold", 2, 4, cmd="CURSOR_DOWN")) |
| 161 | + ui_page1.add(create_ui_icon("uc:left-arrow", 1, 3, cmd="CURSOR_LEFT")) |
| 162 | + ui_page1.add(create_ui_icon("uc:right-arrow", 3, 3, cmd="CURSOR_RIGHT")) |
| 163 | + ui_page1.add(create_ui_text("Ok", 2, 3, cmd="CURSOR_ENTER")) |
| 164 | + |
| 165 | + ui_page2 = UiPage("page2", "Page 2") |
| 166 | + ui_page2.add( |
| 167 | + create_ui_text( |
| 168 | + "Pump up the volume!", |
| 169 | + 0, |
| 170 | + 0, |
| 171 | + size=Size(4, 2), |
| 172 | + cmd=create_send_cmd("VOLUME_UP", repeat=5), |
| 173 | + ) |
| 174 | + ) |
| 175 | + ui_page2.add( |
| 176 | + create_ui_text( |
| 177 | + "Test sequence", |
| 178 | + 0, |
| 179 | + 4, |
| 180 | + size=Size(4, 1), |
| 181 | + cmd=create_sequence_cmd( |
| 182 | + [ |
| 183 | + "CURSOR_UP", |
| 184 | + "CURSOR_RIGHT", |
| 185 | + "CURSOR_DOWN", |
| 186 | + "CURSOR_LEFT", |
| 187 | + ], |
| 188 | + delay=200, |
| 189 | + ), |
| 190 | + ) |
| 191 | + ) |
| 192 | + ui_page2.add(create_ui_text("On", 0, 5, cmd="on")) |
| 193 | + ui_page2.add(create_ui_text("Off", 1, 5, cmd="off")) |
| 194 | + |
| 195 | + return [main_page, ui_page1, ui_page2] |
| 196 | + |
| 197 | + |
| 198 | +if __name__ == "__main__": |
| 199 | + logging.basicConfig() |
| 200 | + |
| 201 | + entity = ucapi.Remote( |
| 202 | + "remote1", |
| 203 | + "Demo remote", |
| 204 | + [remote.Features.ON_OFF, remote.Features.TOGGLE], |
| 205 | + {remote.Attributes.STATE: remote.States.OFF}, |
| 206 | + simple_commands=supported_commands, |
| 207 | + button_mapping=create_button_mappings(), |
| 208 | + ui_pages=create_ui(), |
| 209 | + cmd_handler=cmd_handler, |
| 210 | + ) |
| 211 | + api.available_entities.add(entity) |
| 212 | + |
| 213 | + loop.run_until_complete(api.init("remote.json")) |
| 214 | + loop.run_forever() |
0 commit comments