diff --git a/CHANGELOG.md b/CHANGELOG.md index 18e9773..568db84 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,11 +1,17 @@ -## [3.1.1] +## [5.0.0] +- feat: Speaker, GarageDoor examples added. +- feat: setMode, setRangeValue - instance id support added. +- fix: Signature mismatch issue fixed. +- fix: setSetting command response format. -### Fix +## [4.0.0] -* Logging +- BREAKING CHANGE: Remove `restoreDeviceStates` in order to change this at device level from server side instead of fixed value in client sdk. -## [3.1.0] +## [3.1.1] -### Features +- Fix Logging + +## [3.1.0] -* Replaced with new SDK +- Replaced with new SDK diff --git a/examples/customdevice/customdevice_example.py b/examples/customdevice/customdevice_example.py index 93a32ca..8eeaf78 100644 --- a/examples/customdevice/customdevice_example.py +++ b/examples/customdevice/customdevice_example.py @@ -26,6 +26,22 @@ "color": {"r": 255, "g": 255, "b": 255}, } +async def on_range_value(rangeValue: int, instance_id: str) -> bool: + """Handle range value changes.""" + if instance_id: + print(f"[Range] Instance '{instance_id}' set to {rangeValue}") + else: + print(f"[Range] Set to {rangeValue}") + return True + + +async def on_mode_state(mode: str, instance_id: str) -> bool: + """Handle mode state changes.""" + if instance_id: + print(f"[Mode] Instance '{instance_id}' set to {mode}") + else: + print(f"[Mode] Set to {mode}") + return True async def on_power_state(state: bool) -> bool: """Handle power state changes.""" @@ -85,7 +101,6 @@ async def on_lock_state(state: str) -> bool: print(f"\n[Lock] State set to {state}") return True - async def on_setting(setting: str, value: Any) -> bool: """Handle device setting changes.""" print(f"\n[Setting] {setting} = {value}") @@ -134,6 +149,8 @@ async def main() -> None: # Common custom_device.on_setting(on_setting) + custom_device.on_mode_state(on_mode_state) + custom_device.on_range_value(on_range_value) # Add device to SinricPro sinric_pro.add(custom_device) diff --git a/examples/fan/fan_example.py b/examples/fan/fan_example.py index 85eceb6..d6da09b 100644 --- a/examples/fan/fan_example.py +++ b/examples/fan/fan_example.py @@ -38,12 +38,13 @@ async def on_power_state(state: bool) -> bool: return True -async def on_range_value(speed: int) -> bool: +async def on_range_value(speed: int, instance_id: str) -> bool: """ Handle fan speed change requests. Args: speed: Fan speed level (0-100) + instance_id: Instance ID for multi-instance range control (not used for fan) Returns: True if successful, False otherwise @@ -64,12 +65,13 @@ async def on_range_value(speed: int) -> bool: return True -async def on_adjust_range_value(speed_delta: int) -> bool: +async def on_adjust_range_value(speed_delta: int, instance_id: str) -> bool: """ Handle relative fan speed adjustment requests. Args: speed_delta: Change in speed (-100 to +100) + instance_id: Instance ID for multi-instance range control (not used for fan) Returns: True if successful, False otherwise diff --git a/examples/garagedoor/garage_door_example.py b/examples/garagedoor/garage_door_example.py index f71ae9d..dd83105 100644 --- a/examples/garagedoor/garage_door_example.py +++ b/examples/garagedoor/garage_door_example.py @@ -13,12 +13,13 @@ current_state = "Close" # "Open" or "Close" -async def on_mode_state(state: str) -> bool: +async def on_mode_state(state: str, instance_id: str) -> bool: """ Handle garage door open/close requests. - + Args: state: "Open" or "Close" + instance_id: Instance ID (not used for garage door) """ global current_state print(f"Garage door command: {state}") diff --git a/examples/speaker/speaker_example.py b/examples/speaker/speaker_example.py new file mode 100644 index 0000000..d914c94 --- /dev/null +++ b/examples/speaker/speaker_example.py @@ -0,0 +1,157 @@ +""" +SinricPro Speaker Example + +Demonstrates: +- Power control +- Volume control +- Mute control +- Media playback controls +- Equalizer settings +- Mode selection +""" + +import asyncio +import os + +from sinricpro import SinricPro, SinricProSpeaker, SinricProConfig + +# Device ID from SinricPro portal +DEVICE_ID = "YOUR_DEVICE_ID_HERE" # Replace with your device ID + +# Credentials from SinricPro portal +APP_KEY = os.getenv("SINRICPRO_APP_KEY", "YOUR_APP_KEY_HERE") +APP_SECRET = os.getenv("SINRICPRO_APP_SECRET", "YOUR_APP_SECRET_HERE") + +# Speaker state +speaker_state = { + "power": False, + "volume": 50, + "muted": False, + "mode": "MUSIC", + "equalizer": { + "bass": 0, + "midrange": 0, + "treble": 0, + }, +} + +async def on_power_state(state: bool) -> bool: + """Handle power state changes from SinricPro.""" + print(f"[Power] Speaker turned {'ON' if state else 'OFF'}") + speaker_state["power"] = state + return True + + +async def on_volume(volume: int) -> bool: + """Handle volume changes from SinricPro.""" + print(f"[Volume] Set to {volume}") + speaker_state["volume"] = volume + return True + + +async def on_adjust_volume(volume_delta: int) -> bool: + """Handle volume adjustments from SinricPro.""" + print(f"[Volume] Adjust by {'+' if volume_delta > 0 else ''}{volume_delta}") + speaker_state["volume"] = max(0, min(100, speaker_state["volume"] + volume_delta)) + print(f" New volume: {speaker_state['volume']}") + return True + + +async def on_mute(mute: bool) -> bool: + """Handle mute state changes from SinricPro.""" + print(f"[Mute] {'ON' if mute else 'OFF'}") + speaker_state["muted"] = mute + return True + + +async def on_media_control(control: str) -> bool: + """Handle media control commands from SinricPro.""" + print(f"[Media] {control}") + # control can be: Play, Pause, Stop, Next, Previous, Rewind, FastForward + return True + + +async def on_set_bands(bands: dict) -> bool: + """Handle equalizer band settings from SinricPro.""" + print(f"[Equalizer] Bands set: {bands}") + speaker_state["equalizer"].update(bands) + return True + + +async def on_adjust_bands(bands: dict) -> bool: + """Handle equalizer band adjustments from SinricPro.""" + print(f"[Equalizer] Bands adjusted: {bands}") + for band, delta in bands.items(): + if band in speaker_state["equalizer"]: + speaker_state["equalizer"][band] += delta + return True + + +async def on_mode(mode: str, instance_id: str) -> bool: + """Handle mode changes from SinricPro.""" + print(f"[Mode] Set to {mode}") + speaker_state["mode"] = mode + # Mode can be: MUSIC, MOVIE, NIGHT, SPORT, TV, etc. + return True + + +async def main() -> None: + """Main function.""" + print("=" * 60) + print("SinricPro Smart Speaker Example") + print("=" * 60) + + # Create SinricPro instance + sinric_pro = SinricPro.get_instance() + + # Create speaker device + my_speaker = SinricProSpeaker(DEVICE_ID) + + # Register callbacks + my_speaker.on_power_state(on_power_state) + my_speaker.on_volume(on_volume) + my_speaker.on_adjust_volume(on_adjust_volume) + my_speaker.on_mute(on_mute) + my_speaker.on_media_control(on_media_control) + my_speaker.on_set_bands(on_set_bands) + my_speaker.on_adjust_bands(on_adjust_bands) + my_speaker.on_mode_state(on_mode) + + # Add device to SinricPro + sinric_pro.add(my_speaker) + + # Configure and connect + config = SinricProConfig( + app_key=APP_KEY, + app_secret=APP_SECRET + ) + + try: + print("Connecting to SinricPro...") + await sinric_pro.begin(config) + print("Connected! You can now control your speaker via Alexa or Google Home.") + print() + print("Try saying:") + print(' "Alexa, turn on the speaker"') + print(' "Alexa, set volume to 50"') + print(' "Alexa, mute the speaker"') + print(' "Alexa, play music"') + print(' "Alexa, increase bass"') + print() + print("Press Ctrl+C to exit") + + # Keep the application running + while True: + await asyncio.sleep(1) + + except KeyboardInterrupt: + print("\nShutting down...") + except Exception as e: + print(f"Error: {e}") + finally: + await sinric_pro.stop() + + +if __name__ == "__main__": + # Run the async main function + asyncio.run(main()) diff --git a/examples/temperaturesensor/temperature_sensor_example.py b/examples/temperaturesensor/temperature_sensor_example.py index 71c5165..441bdfe 100644 --- a/examples/temperaturesensor/temperature_sensor_example.py +++ b/examples/temperaturesensor/temperature_sensor_example.py @@ -11,11 +11,11 @@ from sinricpro import SinricPro, SinricProTemperatureSensor, SinricProConfig # Device ID from SinricPro portal -DEVICE_ID = "YOUR_DEVICE_ID_HERE" # Replace with your device ID +DEVICE_ID = "YOUR-DEVICE-ID" # Replace with your device ID # Credentials from SinricPro portal -APP_KEY = os.getenv("SINRICPRO_APP_KEY", "YOUR_APP_KEY_HERE") -APP_SECRET = os.getenv("SINRICPRO_APP_SECRET", "YOUR_APP_SECRET_HERE") +APP_KEY = os.getenv("SINRICPRO_APP_KEY", "YOUR-APP-KEY") +APP_SECRET = os.getenv("SINRICPRO_APP_SECRET", "YOUR-APP-SECRET") async def simulate_sensor_readings(sensor: SinricProTemperatureSensor) -> None: """Simulate temperature/humidity readings for testing.""" diff --git a/examples/tv/tv_example.py b/examples/tv/tv_example.py new file mode 100644 index 0000000..5aaf7fa --- /dev/null +++ b/examples/tv/tv_example.py @@ -0,0 +1,158 @@ +""" +SinricPro TV Example + +Demonstrates: +- Power control +- Volume control +- Mute control +- Channel control +- Input selection +- Media controls +""" + +import asyncio +import os +from typing import TypedDict + +from sinricpro import SinricPro, SinricProTV, SinricProConfig + +# Device ID from SinricPro portal +DEVICE_ID = "YOUR_DEVICE_ID_HERE" # Replace with your device ID + +# Credentials from SinricPro portal +APP_KEY = os.getenv("SINRICPRO_APP_KEY", "YOUR_APP_KEY_HERE") +APP_SECRET = os.getenv("SINRICPRO_APP_SECRET", "YOUR_APP_SECRET_HERE") + + +class ChannelInfo(TypedDict, total=False): + name: str + number: str + + +# TV state +tv_state = { + "power": False, + "volume": 50, + "muted": False, + "channel": {"name": "HBO", "number": "501"}, + "input": "HDMI1", +} + + +async def on_power_state(state: bool) -> bool: + """Handle power state changes from SinricPro.""" + print(f"[Power] TV turned {'ON' if state else 'OFF'}") + tv_state["power"] = state + return True + + +async def on_volume(volume: int) -> bool: + """Handle volume changes from SinricPro.""" + print(f"[Volume] Set to {volume}") + tv_state["volume"] = volume + return True + + +async def on_adjust_volume(volume_delta: int) -> bool: + """Handle volume adjustments from SinricPro.""" + print(f"[Volume] Adjust by {'+' if volume_delta > 0 else ''}{volume_delta}") + tv_state["volume"] = max(0, min(100, tv_state["volume"] + volume_delta)) + print(f" New volume: {tv_state['volume']}") + return True + + +async def on_mute(mute: bool) -> bool: + """Handle mute state changes from SinricPro.""" + print(f"[Mute] {'ON' if mute else 'OFF'}") + tv_state["muted"] = mute + return True + + +async def on_change_channel(channel: ChannelInfo) -> bool: + """Handle channel changes from SinricPro.""" + channel_name = channel.get("name") or channel.get("number") + print(f"[Channel] Changed to: {channel_name}") + tv_state["channel"] = channel + return True + + +async def on_skip_channels(count: int) -> bool: + """Handle channel skip from SinricPro.""" + direction = "forward" if count > 0 else "backward" + print(f"[Channel] Skip {direction} {abs(count)} channels") + return True + + +async def on_select_input(input_name: str) -> bool: + """Handle input selection from SinricPro.""" + print(f"[Input] Switched to: {input_name}") + tv_state["input"] = input_name + return True + + +async def on_media_control(control: str) -> bool: + """Handle media control commands from SinricPro.""" + print(f"[Media] {control}") + return True + + +async def main() -> None: + """Main function.""" + print("=" * 60) + print("SinricPro Smart TV Example") + print("=" * 60) + + # Create SinricPro instance + sinric_pro = SinricPro.get_instance() + + # Create TV device + my_tv = SinricProTV(DEVICE_ID) + + # Register callbacks + my_tv.on_power_state(on_power_state) + my_tv.on_volume(on_volume) + my_tv.on_adjust_volume(on_adjust_volume) + my_tv.on_mute(on_mute) + my_tv.on_change_channel(on_change_channel) + my_tv.on_skip_channels(on_skip_channels) + my_tv.on_select_input(on_select_input) + my_tv.on_media_control(on_media_control) + + # Add device to SinricPro + sinric_pro.add(my_tv) + + # Configure and connect + config = SinricProConfig( + app_key=APP_KEY, + app_secret=APP_SECRET + ) + + try: + print("Connecting to SinricPro...") + await sinric_pro.begin(config) + print("Connected! You can now control your TV via Alexa or Google Home.") + print() + print("Try saying:") + print(' "Alexa, turn on the TV"') + print(' "Alexa, change channel to HBO"') + print(' "Alexa, set volume to 50"') + print(' "Alexa, mute the TV"') + print(' "Alexa, switch to HDMI 1"') + print() + print("Press Ctrl+C to exit") + + # Keep the application running + while True: + await asyncio.sleep(1) + + except KeyboardInterrupt: + print("\nShutting down...") + except Exception as e: + print(f"Error: {e}") + finally: + await sinric_pro.stop() + + +if __name__ == "__main__": + # Run the async main function + asyncio.run(main()) diff --git a/examples/windowac/windowac_example.py b/examples/windowac/windowac_example.py index b0fd189..fdea5f1 100644 --- a/examples/windowac/windowac_example.py +++ b/examples/windowac/windowac_example.py @@ -85,12 +85,13 @@ async def on_target_temperature(temperature: float) -> bool: return True -async def on_range_value(speed: int) -> bool: +async def on_range_value(speed: int, instance_id: str) -> bool: """ Handle fan speed change requests. Args: speed: Fan speed level (0-100) + instance_id: Instance ID for multi-instance range control (not used for AC) Returns: True if successful, False otherwise @@ -112,12 +113,13 @@ async def on_range_value(speed: int) -> bool: return True -async def on_adjust_range_value(speed_delta: int) -> bool: +async def on_adjust_range_value(speed_delta: int, instance_id: str) -> bool: """ Handle relative fan speed adjustment requests. Args: speed_delta: Change in speed (-100 to +100) + instance_id: Instance ID for multi-instance range control (not used for AC) Returns: True if successful, False otherwise diff --git a/llms-full.txt b/llms-full.txt index a6d40e7..02a740c 100644 --- a/llms-full.txt +++ b/llms-full.txt @@ -85,7 +85,6 @@ class SinricPro: class SinricProConfig: app_key: str # UUID format app_secret: str # Min 32 chars - restore_states: bool = True debug: bool = False def __post_init__(self): diff --git a/pyproject.toml b/pyproject.toml index 9e56604..85a6fd4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "sinricpro" -version = "3.1.1" +version = "5.0.0" description = "Official SinricPro SDK for Python - Control IoT devices with Alexa and Google Home" authors = [{name = "SinricPro", email = "support@sinric.pro"}] readme = "README.md" diff --git a/sinricpro/__init__.py b/sinricpro/__init__.py index cf332d4..de69763 100644 --- a/sinricpro/__init__.py +++ b/sinricpro/__init__.py @@ -9,7 +9,7 @@ This file is part of the SinricPro Python SDK (https://github.com/sinricpro/) """ -__version__ = "3.1.1" +__version__ = "5.0.0" from sinricpro.core.sinric_pro import SinricPro, SinricProConfig from sinricpro.core.sinric_pro_device import SinricProDevice @@ -41,7 +41,8 @@ from sinricpro.devices.sinric_pro_doorbell import SinricProDoorbell from sinricpro.devices.sinric_pro_camera import SinricProCamera from sinricpro.devices.sinric_pro_custom_device import SinricProCustomDevice - +from sinricpro.devices.sinric_pro_speaker import SinricProSpeaker +from sinricpro.devices.sinric_pro_tv import SinricProTV # Exceptions from sinricpro.core.exceptions import ( @@ -80,6 +81,8 @@ "SinricProDoorbell", "SinricProCamera", "SinricProCustomDevice", + "SinricProSpeaker", + "SinricProTV", # Logger "SinricProLogger", "LogLevel", diff --git a/sinricpro/capabilities/__init__.py b/sinricpro/capabilities/__init__.py index 39afdf8..c1cab20 100644 --- a/sinricpro/capabilities/__init__.py +++ b/sinricpro/capabilities/__init__.py @@ -23,6 +23,12 @@ from sinricpro.capabilities.setting_controller import SettingController from sinricpro.capabilities.temperature_sensor import TemperatureSensor from sinricpro.capabilities.thermostat_controller import ThermostatController +from sinricpro.capabilities.volume_controller import VolumeController +from sinricpro.capabilities.mute_controller import MuteController +from sinricpro.capabilities.media_controller import MediaController +from sinricpro.capabilities.equalizer_controller import EqualizerController +from sinricpro.capabilities.channel_controller import ChannelController +from sinricpro.capabilities.input_controller import InputController __all__ = [ "AirQualitySensor", @@ -44,4 +50,10 @@ "SettingController", "TemperatureSensor", "ThermostatController", + "VolumeController", + "MuteController", + "MediaController", + "EqualizerController", + "ChannelController", + "InputController", ] diff --git a/sinricpro/capabilities/channel_controller.py b/sinricpro/capabilities/channel_controller.py new file mode 100644 index 0000000..20edcbd --- /dev/null +++ b/sinricpro/capabilities/channel_controller.py @@ -0,0 +1,118 @@ +""" +ChannelController Capability + +Provides TV channel control functionality. +""" + +from typing import Any, Callable, Awaitable, TypedDict, TYPE_CHECKING + +from sinricpro.core.event_limiter import EventLimiter +from sinricpro.core.actions import ACTION_CHANGE_CHANNEL, ACTION_SKIP_CHANNELS +from sinricpro.core.types import EVENT_LIMIT_STATE +from sinricpro.utils.logger import SinricProLogger + +if TYPE_CHECKING: + from sinricpro.core.sinric_pro_device import SinricProDevice + + +class ChannelInfo(TypedDict, total=False): + """Channel information.""" + name: str + number: str + + +ChangeChannelCallback = Callable[[ChannelInfo], Awaitable[bool]] +SkipChannelsCallback = Callable[[int], Awaitable[bool]] + + +class ChannelController: + """Mixin providing TV channel control capability.""" + + def __init__(self, *args: Any, **kwargs: Any) -> None: + """Initialize ChannelController mixin.""" + super().__init__(*args, **kwargs) + self._change_channel_callback: ChangeChannelCallback | None = None + self._skip_channels_callback: SkipChannelsCallback | None = None + self._channel_limiter = EventLimiter(EVENT_LIMIT_STATE) + + def on_change_channel(self, callback: ChangeChannelCallback) -> None: + """Register callback for channel changes. + + Args: + callback: Async function that receives channel info dict and returns True on success. + Channel dict may contain: name, number + """ + self._change_channel_callback = callback + + def on_skip_channels(self, callback: SkipChannelsCallback) -> None: + """Register callback for skipping channels. + + Args: + callback: Async function that receives channel count (positive=forward, negative=backward) + and returns True on success + """ + self._skip_channels_callback = callback + + async def handle_change_channel_request( + self, channel: ChannelInfo, device: "SinricProDevice" + ) -> tuple[bool, dict[str, Any]]: + """Handle changeChannel request.""" + if not self._change_channel_callback: + SinricProLogger.error(f"No change channel callback registered for {device.get_device_id()}") + return False, {} + + try: + success = await self._change_channel_callback(channel) + if success: + return True, {"channel": channel} + else: + return False, {} + except Exception as e: + SinricProLogger.error(f"Error in change channel callback: {e}") + return False, {} + + async def handle_skip_channels_request( + self, channel_count: int, device: "SinricProDevice" + ) -> tuple[bool, dict[str, Any]]: + """Handle skipChannels request.""" + if not self._skip_channels_callback: + SinricProLogger.error(f"No skip channels callback registered for {device.get_device_id()}") + return False, {} + + try: + success = await self._skip_channels_callback(channel_count) + if success: + return True, {"channelCount": channel_count} + else: + return False, {} + except Exception as e: + SinricProLogger.error(f"Error in skip channels callback: {e}") + return False, {} + + async def send_channel_event( + self, channel: ChannelInfo, cause: str = "PHYSICAL_INTERACTION" + ) -> bool: + """Send channel event to SinricPro. + + Args: + channel: Channel information + cause: Cause of the event + + Returns: + True if event was sent successfully + """ + if not self._channel_limiter.can_send_event(): + SinricProLogger.warn("Channel event rate limited") + return False + + if not hasattr(self, "send_event"): + SinricProLogger.error("ChannelController must be mixed with SinricProDevice") + return False + + device: SinricProDevice = self # type: ignore + success = await device.send_event(action=ACTION_CHANGE_CHANNEL, value={"channel": channel}, cause=cause) + + if success: + self._channel_limiter.event_sent() + + return success diff --git a/sinricpro/capabilities/equalizer_controller.py b/sinricpro/capabilities/equalizer_controller.py new file mode 100644 index 0000000..545d88a --- /dev/null +++ b/sinricpro/capabilities/equalizer_controller.py @@ -0,0 +1,120 @@ +""" +EqualizerController Capability + +Provides equalizer control functionality for speakers and audio devices. +Supports bass, midrange, and treble band adjustments. +""" + +from typing import Any, Callable, Awaitable, TypedDict, TYPE_CHECKING + +from sinricpro.core.event_limiter import EventLimiter +from sinricpro.core.actions import ACTION_SET_BANDS, ACTION_ADJUST_BANDS +from sinricpro.core.types import EVENT_LIMIT_STATE +from sinricpro.utils.logger import SinricProLogger + +if TYPE_CHECKING: + from sinricpro.core.sinric_pro_device import SinricProDevice + + +class EqualizerBands(TypedDict, total=False): + """Equalizer bands configuration.""" + bass: int + midrange: int + treble: int + + +SetBandsCallback = Callable[[EqualizerBands], Awaitable[bool]] +AdjustBandsCallback = Callable[[EqualizerBands], Awaitable[bool]] + + +class EqualizerController: + """Mixin providing equalizer control capability.""" + + def __init__(self, *args: Any, **kwargs: Any) -> None: + """Initialize EqualizerController mixin.""" + super().__init__(*args, **kwargs) + self._set_bands_callback: SetBandsCallback | None = None + self._adjust_bands_callback: AdjustBandsCallback | None = None + self._equalizer_limiter = EventLimiter(EVENT_LIMIT_STATE) + + def on_set_bands(self, callback: SetBandsCallback) -> None: + """Register callback for setting equalizer bands. + + Args: + callback: Async function that receives bands dict and returns True on success. + Bands dict may contain: bass, midrange, treble (values typically -10 to 10) + """ + self._set_bands_callback = callback + + def on_adjust_bands(self, callback: AdjustBandsCallback) -> None: + """Register callback for adjusting equalizer bands. + + Args: + callback: Async function that receives bands delta dict and returns True on success. + Bands dict may contain: bass, midrange, treble (delta values) + """ + self._adjust_bands_callback = callback + + async def handle_set_bands_request( + self, bands: EqualizerBands, device: "SinricProDevice" + ) -> tuple[bool, dict[str, Any]]: + """Handle setBands request.""" + if not self._set_bands_callback: + SinricProLogger.error(f"No set bands callback registered for {device.get_device_id()}") + return False, {} + + try: + success = await self._set_bands_callback(bands) + if success: + return True, {"bands": bands} + else: + return False, {} + except Exception as e: + SinricProLogger.error(f"Error in set bands callback: {e}") + return False, {} + + async def handle_adjust_bands_request( + self, bands: EqualizerBands, device: "SinricProDevice" + ) -> tuple[bool, dict[str, Any]]: + """Handle adjustBands request.""" + if not self._adjust_bands_callback: + SinricProLogger.error(f"No adjust bands callback registered for {device.get_device_id()}") + return False, {} + + try: + success = await self._adjust_bands_callback(bands) + if success: + return True, {"bands": bands} + else: + return False, {} + except Exception as e: + SinricProLogger.error(f"Error in adjust bands callback: {e}") + return False, {} + + async def send_bands_event( + self, bands: EqualizerBands, cause: str = "PHYSICAL_INTERACTION" + ) -> bool: + """Send equalizer bands event to SinricPro. + + Args: + bands: Equalizer bands configuration + cause: Cause of the event + + Returns: + True if event was sent successfully + """ + if not self._equalizer_limiter.can_send_event(): + SinricProLogger.warn("Equalizer bands event rate limited") + return False + + if not hasattr(self, "send_event"): + SinricProLogger.error("EqualizerController must be mixed with SinricProDevice") + return False + + device: SinricProDevice = self # type: ignore + success = await device.send_event(action=ACTION_SET_BANDS, value={"bands": bands}, cause=cause) + + if success: + self._equalizer_limiter.event_sent() + + return success diff --git a/sinricpro/capabilities/input_controller.py b/sinricpro/capabilities/input_controller.py new file mode 100644 index 0000000..ec3f4d4 --- /dev/null +++ b/sinricpro/capabilities/input_controller.py @@ -0,0 +1,82 @@ +""" +InputController Capability + +Provides input selection functionality for TVs and AV receivers. +""" + +from typing import Any, Callable, Awaitable, TYPE_CHECKING + +from sinricpro.core.event_limiter import EventLimiter +from sinricpro.core.actions import ACTION_SELECT_INPUT +from sinricpro.core.types import EVENT_LIMIT_STATE +from sinricpro.utils.logger import SinricProLogger + +if TYPE_CHECKING: + from sinricpro.core.sinric_pro_device import SinricProDevice + +SelectInputCallback = Callable[[str], Awaitable[bool]] + + +class InputController: + """Mixin providing input selection capability.""" + + def __init__(self, *args: Any, **kwargs: Any) -> None: + """Initialize InputController mixin.""" + super().__init__(*args, **kwargs) + self._select_input_callback: SelectInputCallback | None = None + self._input_limiter = EventLimiter(EVENT_LIMIT_STATE) + + def on_select_input(self, callback: SelectInputCallback) -> None: + """Register callback for input selection. + + Args: + callback: Async function that receives input name (e.g., HDMI1, HDMI2, TV, etc.) + and returns True on success + """ + self._select_input_callback = callback + + async def handle_select_input_request( + self, input_name: str, device: "SinricProDevice" + ) -> tuple[bool, dict[str, Any]]: + """Handle selectInput request.""" + if not self._select_input_callback: + SinricProLogger.error(f"No select input callback registered for {device.get_device_id()}") + return False, {} + + try: + success = await self._select_input_callback(input_name) + if success: + return True, {"input": input_name} + else: + return False, {} + except Exception as e: + SinricProLogger.error(f"Error in select input callback: {e}") + return False, {} + + async def send_input_event( + self, input_name: str, cause: str = "PHYSICAL_INTERACTION" + ) -> bool: + """Send input selection event to SinricPro. + + Args: + input_name: Input name (e.g., HDMI1, HDMI2, TV) + cause: Cause of the event + + Returns: + True if event was sent successfully + """ + if not self._input_limiter.can_send_event(): + SinricProLogger.warn("Input event rate limited") + return False + + if not hasattr(self, "send_event"): + SinricProLogger.error("InputController must be mixed with SinricProDevice") + return False + + device: SinricProDevice = self # type: ignore + success = await device.send_event(action=ACTION_SELECT_INPUT, value={"input": input_name}, cause=cause) + + if success: + self._input_limiter.event_sent() + + return success diff --git a/sinricpro/capabilities/media_controller.py b/sinricpro/capabilities/media_controller.py new file mode 100644 index 0000000..1c6c55e --- /dev/null +++ b/sinricpro/capabilities/media_controller.py @@ -0,0 +1,83 @@ +""" +MediaController Capability + +Provides media playback control functionality for speakers, TVs, and media devices. +""" + +from typing import Any, Callable, Awaitable, Literal, TYPE_CHECKING + +from sinricpro.core.event_limiter import EventLimiter +from sinricpro.core.actions import ACTION_MEDIA_CONTROL +from sinricpro.core.types import EVENT_LIMIT_STATE +from sinricpro.utils.logger import SinricProLogger + +if TYPE_CHECKING: + from sinricpro.core.sinric_pro_device import SinricProDevice + +MediaControl = Literal["Play", "Pause", "Stop", "StartOver", "Previous", "Next", "Rewind", "FastForward"] +MediaControlCallback = Callable[[MediaControl], Awaitable[bool]] + + +class MediaController: + """Mixin providing media playback control capability.""" + + def __init__(self, *args: Any, **kwargs: Any) -> None: + """Initialize MediaController mixin.""" + super().__init__(*args, **kwargs) + self._media_control_callback: MediaControlCallback | None = None + self._media_limiter = EventLimiter(EVENT_LIMIT_STATE) + + def on_media_control(self, callback: MediaControlCallback) -> None: + """Register callback for media control commands. + + Args: + callback: Async function that receives media control command and returns True on success. + Commands: Play, Pause, Stop, StartOver, Previous, Next, Rewind, FastForward + """ + self._media_control_callback = callback + + async def handle_media_control_request( + self, control: MediaControl, device: "SinricProDevice" + ) -> tuple[bool, dict[str, Any]]: + """Handle mediaControl request.""" + if not self._media_control_callback: + SinricProLogger.error(f"No media control callback registered for {device.get_device_id()}") + return False, {} + + try: + success = await self._media_control_callback(control) + if success: + return True, {"control": control} + else: + return False, {} + except Exception as e: + SinricProLogger.error(f"Error in media control callback: {e}") + return False, {} + + async def send_media_control_event( + self, control: MediaControl, cause: str = "PHYSICAL_INTERACTION" + ) -> bool: + """Send media control event to SinricPro. + + Args: + control: Media control command (Play, Pause, Stop, etc.) + cause: Cause of the event + + Returns: + True if event was sent successfully + """ + if not self._media_limiter.can_send_event(): + SinricProLogger.warn("Media control event rate limited") + return False + + if not hasattr(self, "send_event"): + SinricProLogger.error("MediaController must be mixed with SinricProDevice") + return False + + device: SinricProDevice = self # type: ignore + success = await device.send_event(action=ACTION_MEDIA_CONTROL, value={"control": control}, cause=cause) + + if success: + self._media_limiter.event_sent() + + return success diff --git a/sinricpro/capabilities/mode_controller.py b/sinricpro/capabilities/mode_controller.py index 1d83912..e4db558 100644 --- a/sinricpro/capabilities/mode_controller.py +++ b/sinricpro/capabilities/mode_controller.py @@ -1,7 +1,8 @@ """ ModeController Capability -Provides mode control functionality (open/close) for garage doors, etc. +Provides mode control functionality for devices that support multiple modes. +Supports optional instanceId for multi-instance mode control. """ from typing import Any, Callable, Awaitable, TYPE_CHECKING @@ -14,11 +15,12 @@ if TYPE_CHECKING: from sinricpro.core.sinric_pro_device import SinricProDevice -ModeStateCallback = Callable[[str], Awaitable[bool]] +# Callback receives mode and optional instance_id +ModeStateCallback = Callable[[str, str], Awaitable[bool]] class ModeController: - """Mixin providing door control capability.""" + """Mixin providing mode control capability.""" def __init__(self, *args: Any, **kwargs: Any) -> None: """Initialize ModeController mixin.""" @@ -27,31 +29,57 @@ def __init__(self, *args: Any, **kwargs: Any) -> None: self._mode_limiter = EventLimiter(EVENT_LIMIT_STATE) def on_mode_state(self, callback: ModeStateCallback) -> None: - """Register callback for mode changes.""" + """Register callback for mode changes. + + Args: + callback: Async function that receives mode and instance_id, returns True on success. + Signature: async def callback(mode: str, instance_id: str) -> bool + instance_id will be empty string if not provided by server. + """ self._mode_state_callback = callback async def handle_mode_request( - self, mode: str, device: "SinricProDevice" + self, mode: str, instance_id: str, device: "SinricProDevice" ) -> tuple[bool, dict[str, Any]]: - """Handle setMode request.""" + """Handle setMode request. + + Args: + mode: The mode value to set + instance_id: Optional instance ID for multi-instance mode control + device: The device handling this request + + Returns: + Tuple of (success, response_value) + """ if not self._mode_state_callback: - SinricProLogger.error(f"No door state callback registered for {device.get_device_id()}") + SinricProLogger.error(f"No mode state callback registered for {device.get_device_id()}") return False, {} try: - success = await self._mode_state_callback(mode) + success = await self._mode_state_callback(mode, instance_id) if success: return True, {"mode": mode} else: return False, {} except Exception as e: - SinricProLogger.error(f"Error in door state callback: {e}") + SinricProLogger.error(f"Error in mode state callback: {e}") return False, {} - async def send_mode_event(self, mode: str, cause: str = "PHYSICAL_INTERACTION") -> bool: - """Send door mode event (Open/Close).""" + async def send_mode_event( + self, mode: str, instance_id: str = "", cause: str = "PHYSICAL_INTERACTION" + ) -> bool: + """Send mode event to SinricPro. + + Args: + mode: The mode value + instance_id: Optional instance ID for multi-instance mode control + cause: Cause of the event + + Returns: + True if event was sent successfully + """ if not self._mode_limiter.can_send_event(): - SinricProLogger.warn("Door mode event rate limited") + SinricProLogger.warn("Mode event rate limited") return False if not hasattr(self, "send_event"): @@ -59,7 +87,12 @@ async def send_mode_event(self, mode: str, cause: str = "PHYSICAL_INTERACTION") return False device: SinricProDevice = self # type: ignore - success = await device.send_event(action=ACTION_SET_MODE, value={"mode": mode}, cause=cause) + success = await device.send_event( + action=ACTION_SET_MODE, + value={"mode": mode}, + cause=cause, + instance_id=instance_id + ) if success: self._mode_limiter.event_sent() diff --git a/sinricpro/capabilities/mute_controller.py b/sinricpro/capabilities/mute_controller.py new file mode 100644 index 0000000..8bf8285 --- /dev/null +++ b/sinricpro/capabilities/mute_controller.py @@ -0,0 +1,79 @@ +""" +MuteController Capability + +Provides mute/unmute functionality for speakers, TVs, and other audio devices. +""" + +from typing import Any, Callable, Awaitable, TYPE_CHECKING + +from sinricpro.core.event_limiter import EventLimiter +from sinricpro.core.actions import ACTION_SET_MUTE +from sinricpro.core.types import EVENT_LIMIT_STATE +from sinricpro.utils.logger import SinricProLogger + +if TYPE_CHECKING: + from sinricpro.core.sinric_pro_device import SinricProDevice + +MuteCallback = Callable[[bool], Awaitable[bool]] + + +class MuteController: + """Mixin providing mute control capability.""" + + def __init__(self, *args: Any, **kwargs: Any) -> None: + """Initialize MuteController mixin.""" + super().__init__(*args, **kwargs) + self._mute_callback: MuteCallback | None = None + self._mute_limiter = EventLimiter(EVENT_LIMIT_STATE) + + def on_mute(self, callback: MuteCallback) -> None: + """Register callback for mute changes. + + Args: + callback: Async function that receives mute state (True=muted) and returns True on success + """ + self._mute_callback = callback + + async def handle_mute_request( + self, mute: bool, device: "SinricProDevice" + ) -> tuple[bool, dict[str, Any]]: + """Handle setMute request.""" + if not self._mute_callback: + SinricProLogger.error(f"No mute callback registered for {device.get_device_id()}") + return False, {} + + try: + success = await self._mute_callback(mute) + if success: + return True, {"mute": mute} + else: + return False, {} + except Exception as e: + SinricProLogger.error(f"Error in mute callback: {e}") + return False, {} + + async def send_mute_event(self, mute: bool, cause: str = "PHYSICAL_INTERACTION") -> bool: + """Send mute event to SinricPro. + + Args: + mute: True if muted, False if unmuted + cause: Cause of the event + + Returns: + True if event was sent successfully + """ + if not self._mute_limiter.can_send_event(): + SinricProLogger.warn("Mute event rate limited") + return False + + if not hasattr(self, "send_event"): + SinricProLogger.error("MuteController must be mixed with SinricProDevice") + return False + + device: SinricProDevice = self # type: ignore + success = await device.send_event(action=ACTION_SET_MUTE, value={"mute": mute}, cause=cause) + + if success: + self._mute_limiter.event_sent() + + return success diff --git a/sinricpro/capabilities/range_controller.py b/sinricpro/capabilities/range_controller.py index f11509b..f5c40e4 100644 --- a/sinricpro/capabilities/range_controller.py +++ b/sinricpro/capabilities/range_controller.py @@ -8,11 +8,14 @@ if TYPE_CHECKING: from sinricpro.core.sinric_pro_device import SinricProDevice -RangeValueCallback = Callable[[int], Awaitable[bool]] -AdjustRangeValueCallback = Callable[[int], Awaitable[bool]] +# Callbacks receive value and instance_id +RangeValueCallback = Callable[[int, str], Awaitable[bool]] +AdjustRangeValueCallback = Callable[[int, str], Awaitable[bool]] + class RangeController: """Mixin providing range value control capability.""" + def __init__(self, *args: Any, **kwargs: Any) -> None: super().__init__(*args, **kwargs) self._range_value_callback: RangeValueCallback | None = None @@ -20,43 +23,70 @@ def __init__(self, *args: Any, **kwargs: Any) -> None: self._range_limiter = EventLimiter(EVENT_LIMIT_STATE) def on_range_value(self, callback: RangeValueCallback) -> None: - """Register callback for range value changes.""" + """Register callback for range value changes. + + Args: + callback: Async function that receives range_value and instance_id, returns True on success. + Signature: async def callback(range_value: int, instance_id: str) -> bool + """ self._range_value_callback = callback def on_adjust_range_value(self, callback: AdjustRangeValueCallback) -> None: - """Register callback for relative range value changes.""" + """Register callback for relative range value changes. + + Args: + callback: Async function that receives range_value_delta and instance_id, returns True on success. + Signature: async def callback(range_value_delta: int, instance_id: str) -> bool + """ self._adjust_range_value_callback = callback - async def handle_range_value_request(self, range_value: int, device: "SinricProDevice") -> tuple[bool, dict[str, Any]]: + async def handle_range_value_request( + self, range_value: int, instance_id: str, device: "SinricProDevice" + ) -> tuple[bool, dict[str, Any]]: """Handle setRangeValue request.""" if not self._range_value_callback: return False, {} try: - success = await self._range_value_callback(range_value) + success = await self._range_value_callback(range_value, instance_id) return (True, {"rangeValue": range_value}) if success else (False, {}) except Exception as e: SinricProLogger.error(f"Error in range value callback: {e}") return False, {} - async def handle_adjust_range_value_request(self, range_value_delta: int, device: "SinricProDevice") -> tuple[bool, dict[str, Any]]: + async def handle_adjust_range_value_request( + self, range_value_delta: int, instance_id: str, device: "SinricProDevice" + ) -> tuple[bool, dict[str, Any]]: """Handle adjustRangeValue request.""" if not self._adjust_range_value_callback: return False, {} try: - success = await self._adjust_range_value_callback(range_value_delta) + success = await self._adjust_range_value_callback(range_value_delta, instance_id) return (True, {"rangeValue": range_value_delta}) if success else (False, {}) except Exception as e: SinricProLogger.error(f"Error in adjust range value callback: {e}") return False, {} - async def send_range_value_event(self, range_value: int, cause: str = "PHYSICAL_INTERACTION") -> bool: - """Send range value event.""" + async def send_range_value_event( + self, range_value: int, instance_id: str = "", cause: str = "PHYSICAL_INTERACTION" + ) -> bool: + """Send range value event. + + Args: + range_value: The range value + instance_id: Optional instance ID for multi-instance range control + cause: Cause of the event + """ if not self._range_limiter.can_send_event(): return False if not hasattr(self, "send_event"): return False device: SinricProDevice = self # type: ignore - success = await device.send_event(action=ACTION_SET_RANGE_VALUE, value={"rangeValue": range_value}, cause=cause) + success = await device.send_event( + action=ACTION_SET_RANGE_VALUE, + value={"rangeValue": range_value}, + cause=cause, + instance_id=instance_id + ) if success: self._range_limiter.event_sent() return success diff --git a/sinricpro/capabilities/setting_controller.py b/sinricpro/capabilities/setting_controller.py index 0f31b7d..cd42152 100644 --- a/sinricpro/capabilities/setting_controller.py +++ b/sinricpro/capabilities/setting_controller.py @@ -14,17 +14,17 @@ def __init__(self, *args: Any, **kwargs: Any) -> None: self._setting_callback: SettingCallback | None = None def on_setting(self, callback: SettingCallback) -> None: - """Register callback for setting changes (setting name, value).""" + """Register callback for setting changes (setting id, value).""" self._setting_callback = callback - async def handle_setting_request(self, setting: str, value: Any, device: "SinricProDevice") -> tuple[bool, dict[str, Any]]: + async def handle_setting_request(self, setting_id: str, value: Any, device: "SinricProDevice") -> tuple[bool, dict[str, Any]]: """Handle setSetting request.""" if not self._setting_callback: SinricProLogger.error(f"No setting callback registered for {device.get_device_id()}") return False, {} try: - success = await self._setting_callback(setting, value) - return (True, {"setting": setting, "value": value}) if success else (False, {}) + success = await self._setting_callback(setting_id, value) + return (True, {"id": setting_id, "value": value}) if success else (False, {}) except Exception as e: SinricProLogger.error(f"Error in setting callback: {e}") return False, {} diff --git a/sinricpro/capabilities/volume_controller.py b/sinricpro/capabilities/volume_controller.py new file mode 100644 index 0000000..28d1eeb --- /dev/null +++ b/sinricpro/capabilities/volume_controller.py @@ -0,0 +1,107 @@ +""" +VolumeController Capability + +Provides volume control functionality for speakers, TVs, and other audio devices. +""" + +from typing import Any, Callable, Awaitable, TYPE_CHECKING + +from sinricpro.core.event_limiter import EventLimiter +from sinricpro.core.actions import ACTION_SET_VOLUME, ACTION_ADJUST_VOLUME +from sinricpro.core.types import EVENT_LIMIT_STATE +from sinricpro.utils.logger import SinricProLogger + +if TYPE_CHECKING: + from sinricpro.core.sinric_pro_device import SinricProDevice + +VolumeCallback = Callable[[int], Awaitable[bool]] +AdjustVolumeCallback = Callable[[int], Awaitable[bool]] + + +class VolumeController: + """Mixin providing volume control capability.""" + + def __init__(self, *args: Any, **kwargs: Any) -> None: + """Initialize VolumeController mixin.""" + super().__init__(*args, **kwargs) + self._volume_callback: VolumeCallback | None = None + self._adjust_volume_callback: AdjustVolumeCallback | None = None + self._volume_limiter = EventLimiter(EVENT_LIMIT_STATE) + + def on_volume(self, callback: VolumeCallback) -> None: + """Register callback for volume changes. + + Args: + callback: Async function that receives volume (0-100) and returns True on success + """ + self._volume_callback = callback + + def on_adjust_volume(self, callback: AdjustVolumeCallback) -> None: + """Register callback for volume adjustments. + + Args: + callback: Async function that receives volume delta and returns True on success + """ + self._adjust_volume_callback = callback + + async def handle_volume_request( + self, volume: int, device: "SinricProDevice" + ) -> tuple[bool, dict[str, Any]]: + """Handle setVolume request.""" + if not self._volume_callback: + SinricProLogger.error(f"No volume callback registered for {device.get_device_id()}") + return False, {} + + try: + success = await self._volume_callback(volume) + if success: + return True, {"volume": volume} + else: + return False, {} + except Exception as e: + SinricProLogger.error(f"Error in volume callback: {e}") + return False, {} + + async def handle_adjust_volume_request( + self, volume_delta: int, device: "SinricProDevice" + ) -> tuple[bool, dict[str, Any]]: + """Handle adjustVolume request.""" + if not self._adjust_volume_callback: + SinricProLogger.error(f"No adjust volume callback registered for {device.get_device_id()}") + return False, {} + + try: + success = await self._adjust_volume_callback(volume_delta) + if success: + return True, {"volume": volume_delta} + else: + return False, {} + except Exception as e: + SinricProLogger.error(f"Error in adjust volume callback: {e}") + return False, {} + + async def send_volume_event(self, volume: int, cause: str = "PHYSICAL_INTERACTION") -> bool: + """Send volume event to SinricPro. + + Args: + volume: Volume level (0-100) + cause: Cause of the event + + Returns: + True if event was sent successfully + """ + if not self._volume_limiter.can_send_event(): + SinricProLogger.warn("Volume event rate limited") + return False + + if not hasattr(self, "send_event"): + SinricProLogger.error("VolumeController must be mixed with SinricProDevice") + return False + + device: SinricProDevice = self # type: ignore + success = await device.send_event(action=ACTION_SET_VOLUME, value={"volume": volume}, cause=cause) + + if success: + self._volume_limiter.event_sent() + + return success diff --git a/sinricpro/core/actions.py b/sinricpro/core/actions.py index 056728a..8b2e4b3 100644 --- a/sinricpro/core/actions.py +++ b/sinricpro/core/actions.py @@ -56,6 +56,27 @@ ACTION_GET_WEBRTC_ANSWER = 'getWebRTCAnswer' ACTION_GET_CAMERA_STREAM_URL = 'getCameraStreamUrl' +# Volume Control Actions +ACTION_SET_VOLUME = "setVolume" +ACTION_ADJUST_VOLUME = "adjustVolume" + +# Mute Control Actions +ACTION_SET_MUTE = "setMute" + +# Media Control Actions +ACTION_MEDIA_CONTROL = "mediaControl" + +# Equalizer Control Actions +ACTION_SET_BANDS = "setBands" +ACTION_ADJUST_BANDS = "adjustBands" + +# Channel Control Actions +ACTION_CHANGE_CHANNEL = "changeChannel" +ACTION_SKIP_CHANNELS = "skipChannels" + +# Input Control Actions +ACTION_SELECT_INPUT = "selectInput" + # Percentage Control Actions (Legacy) ACTION_SET_PERCENTAGE = "setPercentage" @@ -101,6 +122,21 @@ "ACTION_GET_SNAPSHOT", "ACTION_GET_WEBRTC_ANSWER", "ACTION_GET_CAMERA_STREAM_URL", + # Volume Control + "ACTION_SET_VOLUME", + "ACTION_ADJUST_VOLUME", + # Mute Control + "ACTION_SET_MUTE", + # Media Control + "ACTION_MEDIA_CONTROL", + # Equalizer Control + "ACTION_SET_BANDS", + "ACTION_ADJUST_BANDS", + # Channel Control + "ACTION_CHANGE_CHANNEL", + "ACTION_SKIP_CHANNELS", + # Input Control + "ACTION_SELECT_INPUT", # Legacy "ACTION_SET_PERCENTAGE", ] diff --git a/sinricpro/core/sinric_pro.py b/sinricpro/core/sinric_pro.py index df74460..30ded79 100644 --- a/sinricpro/core/sinric_pro.py +++ b/sinricpro/core/sinric_pro.py @@ -119,7 +119,6 @@ async def begin(self, config: SinricProConfig | dict[str, Any]) -> None: server_url=self.config.server_url, app_key=self.config.app_key, device_ids=list(self.devices.keys()), - restore_device_states=self.config.restore_device_states, ) self.websocket = WebSocketClient(ws_config) @@ -276,7 +275,7 @@ async def send_message(self, message: dict[str, Any]) -> None: self.signature.sign(message) # Add to send queue - self.send_queue.push_sync(json.dumps(message)) + self.send_queue.push_sync(json.dumps(message, separators=(",", ":"), sort_keys=False)) def get_timestamp(self) -> int: """ @@ -416,13 +415,13 @@ def _send_response( }, } - if self.signature: - self.signature.sign(response_message) - if "instanceId" in request_message["payload"]: response_message["payload"]["instanceId"] = request_message["payload"]["instanceId"] - self.send_queue.push_sync(json.dumps(response_message)) + if self.signature: + self.signature.sign(response_message) + + self.send_queue.push_sync(json.dumps(response_message, separators=(",", ":"), sort_keys=False)) def _send_error_response(self, message: dict[str, Any], error_message: str) -> None: """Send an error response.""" diff --git a/sinricpro/core/sinric_pro_device.py b/sinricpro/core/sinric_pro_device.py index c116500..d8a664d 100644 --- a/sinricpro/core/sinric_pro_device.py +++ b/sinricpro/core/sinric_pro_device.py @@ -104,6 +104,7 @@ async def send_event( action: str, value: dict[str, Any], cause: str = "PHYSICAL_INTERACTION", + instance_id: str = "", ) -> bool: """ Send an event to SinricPro. @@ -112,6 +113,7 @@ async def send_event( action: The action type (e.g., "setPowerState") value: Event data cause: Cause of the event (PHYSICAL_INTERACTION or APP_INTERACTION) + instance_id: Optional instance ID for multi-instance capabilities Returns: True if event was sent successfully, False if rate limited @@ -124,19 +126,25 @@ async def send_event( SinricProLogger.error("Device not added to SinricPro instance") return False + payload: dict[str, Any] = { + "action": action, + "cause": {"type": cause}, + "createdAt": self._sinric_pro.get_timestamp(), + "deviceId": self._device_id, + "type": "event", + "value": value, + } + + # Include instanceId if provided + if instance_id: + payload["instanceId"] = instance_id + message = { "header": { "payloadVersion": 2, "signatureVersion": 1, }, - "payload": { - "action": action, - "cause": {"type": cause}, - "createdAt": self._sinric_pro.get_timestamp(), - "deviceId": self._device_id, - "type": "event", - "value": value, - }, + "payload": payload, } try: diff --git a/sinricpro/core/types.py b/sinricpro/core/types.py index a53381d..f1016fb 100644 --- a/sinricpro/core/types.py +++ b/sinricpro/core/types.py @@ -42,14 +42,12 @@ class SinricProConfig: app_key: SinricPro app key (UUID format) app_secret: SinricPro app secret (min 32 characters) server_url: WebSocket server URL (default: ws.sinric.pro) - restore_device_states: Whether to restore device states on connect debug: Enable debug logging """ app_key: str app_secret: str server_url: str = SINRICPRO_SERVER_URL - restore_device_states: bool = False debug: bool = False def __post_init__(self) -> None: diff --git a/sinricpro/core/websocket_client.py b/sinricpro/core/websocket_client.py index a76fef0..7fad025 100644 --- a/sinricpro/core/websocket_client.py +++ b/sinricpro/core/websocket_client.py @@ -30,14 +30,12 @@ def __init__( server_url: str, app_key: str, device_ids: list[str], - restore_device_states: bool = False, platform: str = "Python", sdk_version: str | None = None, ) -> None: self.server_url = server_url self.app_key = app_key self.device_ids = device_ids - self.restore_device_states = restore_device_states self.platform = platform self.sdk_version = sdk_version or __version__ @@ -108,7 +106,6 @@ async def connect(self) -> None: headers = { "appkey": self.config.app_key, "deviceids": ";".join(self.config.device_ids), - "restoredevicestates": str(self.config.restore_device_states).lower(), "platform": self.config.platform, "SDKVersion": self.config.sdk_version, } diff --git a/sinricpro/devices/__init__.py b/sinricpro/devices/__init__.py index dc841db..e02187d 100644 --- a/sinricpro/devices/__init__.py +++ b/sinricpro/devices/__init__.py @@ -29,6 +29,8 @@ from sinricpro.devices.sinric_pro_fan import SinricProFan from sinricpro.devices.sinric_pro_doorbell import SinricProDoorbell from sinricpro.devices.sinric_pro_camera import SinricProCamera +from sinricpro.devices.sinric_pro_speaker import SinricProSpeaker +from sinricpro.devices.sinric_pro_tv import SinricProTV # Custom from sinricpro.devices.sinric_pro_custom_device import SinricProCustomDevice @@ -48,6 +50,8 @@ "SinricProBlinds", "SinricProGarageDoor", "SinricProLock", + "SinricProSpeaker", + "SinricProTV", # Climate Control "SinricProThermostat", "SinricProWindowAC", diff --git a/sinricpro/devices/sinric_pro_air_quality_sensor.py b/sinricpro/devices/sinric_pro_air_quality_sensor.py index 2e16c31..17bcd22 100644 --- a/sinricpro/devices/sinric_pro_air_quality_sensor.py +++ b/sinricpro/devices/sinric_pro_air_quality_sensor.py @@ -13,9 +13,9 @@ def __init__(self, device_id: str) -> None: super().__init__(device_id=device_id, product_type="AIR_QUALITY_SENSOR") async def handle_request(self, request: SinricProRequest) -> bool: if request.action == ACTION_SET_SETTING: - setting = request.request_value.get("setting", "") + setting_id = request.request_value.get("id", "") value = request.request_value.get("value") - success, response_value = await self.handle_setting_request(setting, value, self) + success, response_value = await self.handle_setting_request(setting_id, value, self) request.response_value = response_value return success request.error_message = f"Missing callback function: {request.action}" diff --git a/sinricpro/devices/sinric_pro_blinds.py b/sinricpro/devices/sinric_pro_blinds.py index 7c72fa7..af9e3db 100644 --- a/sinricpro/devices/sinric_pro_blinds.py +++ b/sinricpro/devices/sinric_pro_blinds.py @@ -24,9 +24,9 @@ async def handle_request(self, request: SinricProRequest) -> bool: request.response_value = response_value return success elif request.action == ACTION_SET_SETTING: - setting = request.request_value.get("setting", "") + setting_id = request.request_value.get("id", "") value = request.request_value.get("value") - success, response_value = await self.handle_setting_request(setting, value, self) + success, response_value = await self.handle_setting_request(setting_id, value, self) request.response_value = response_value return success request.error_message = f"Missing callback function: {request.action}" diff --git a/sinricpro/devices/sinric_pro_camera.py b/sinricpro/devices/sinric_pro_camera.py index 12d29b6..70618f5 100644 --- a/sinricpro/devices/sinric_pro_camera.py +++ b/sinricpro/devices/sinric_pro_camera.py @@ -76,9 +76,9 @@ async def handle_request(self, request: SinricProRequest) -> bool: return success elif request.action == ACTION_SET_SETTING: - setting = request.request_value.get("setting", "") + setting_id = request.request_value.get("id", "") value = request.request_value.get("value") - success, response_value = await self.handle_setting_request(setting, value, self) + success, response_value = await self.handle_setting_request(setting_id, value, self) request.response_value = response_value return success diff --git a/sinricpro/devices/sinric_pro_contact_sensor.py b/sinricpro/devices/sinric_pro_contact_sensor.py index b752dec..50debaa 100644 --- a/sinricpro/devices/sinric_pro_contact_sensor.py +++ b/sinricpro/devices/sinric_pro_contact_sensor.py @@ -12,9 +12,9 @@ def __init__(self, device_id: str) -> None: super().__init__(device_id=device_id, product_type="CONTACT_SENSOR") async def handle_request(self, request: SinricProRequest) -> bool: if request.action == ACTION_SET_SETTING: - setting = request.request_value.get("setting", "") + setting_id = request.request_value.get("id", "") value = request.request_value.get("value") - success, response_value = await self.handle_setting_request(setting, value, self) + success, response_value = await self.handle_setting_request(setting_id, value, self) request.response_value = response_value return success request.error_message = f"Missing callback function: {request.action}" diff --git a/sinricpro/devices/sinric_pro_custom_device.py b/sinricpro/devices/sinric_pro_custom_device.py index 9cd9482..46b8242 100644 --- a/sinricpro/devices/sinric_pro_custom_device.py +++ b/sinricpro/devices/sinric_pro_custom_device.py @@ -243,14 +243,16 @@ async def handle_request(self, request: SinricProRequest) -> bool: # Range Control elif action == ACTION_SET_RANGE_VALUE: range_value = request.request_value.get("rangeValue", 0) - success, response_value = await self.handle_range_value_request(range_value, self) + instance_id = request.instance + success, response_value = await self.handle_range_value_request(range_value, instance_id, self) request.response_value = response_value return success elif action == ACTION_ADJUST_RANGE_VALUE: range_value_delta = request.request_value.get("rangeValueDelta", 0) + instance_id = request.instance success, response_value = await self.handle_adjust_range_value_request( - range_value_delta, self + range_value_delta, instance_id, self ) request.response_value = response_value return success @@ -272,7 +274,8 @@ async def handle_request(self, request: SinricProRequest) -> bool: # Mode Control elif action == ACTION_SET_MODE: mode = request.request_value.get("mode", "") - success, response_value = await self.handle_mode_request(mode, self) + instance_id = request.instance + success, response_value = await self.handle_mode_request(mode, instance_id, self) request.response_value = response_value return success @@ -313,9 +316,9 @@ async def handle_request(self, request: SinricProRequest) -> bool: # Settings Control elif action == ACTION_SET_SETTING: - setting = request.request_value.get("setting", "") + setting_id = request.request_value.get("id", "") value = request.request_value.get("value") - success, response_value = await self.handle_setting_request(setting, value, self) + success, response_value = await self.handle_setting_request(setting_id, value, self) request.response_value = response_value return success diff --git a/sinricpro/devices/sinric_pro_dimswitch.py b/sinricpro/devices/sinric_pro_dimswitch.py index e55521e..a6d22ef 100644 --- a/sinricpro/devices/sinric_pro_dimswitch.py +++ b/sinricpro/devices/sinric_pro_dimswitch.py @@ -34,9 +34,9 @@ async def handle_request(self, request: SinricProRequest) -> bool: request.response_value = response_value return success elif request.action == ACTION_SET_SETTING: - setting = request.request_value.get("setting", "") + setting_id = request.request_value.get("id", "") value = request.request_value.get("value") - success, response_value = await self.handle_setting_request(setting, value, self) + success, response_value = await self.handle_setting_request(setting_id, value, self) request.response_value = response_value return success request.error_message = f"Missing callback function: {request.action}" diff --git a/sinricpro/devices/sinric_pro_doorbell.py b/sinricpro/devices/sinric_pro_doorbell.py index afb7db6..595587e 100644 --- a/sinricpro/devices/sinric_pro_doorbell.py +++ b/sinricpro/devices/sinric_pro_doorbell.py @@ -13,9 +13,9 @@ def __init__(self, device_id: str) -> None: self._doorbell_limiter = EventLimiter(EVENT_LIMIT_STATE) async def handle_request(self, request: SinricProRequest) -> bool: if request.action == ACTION_SET_SETTING: - setting = request.request_value.get("setting", "") + setting_id = request.request_value.get("id", "") value = request.request_value.get("value") - success, response_value = await self.handle_setting_request(setting, value, self) + success, response_value = await self.handle_setting_request(setting_id, value, self) request.response_value = response_value return success request.error_message = f"Missing callback function: {request.action}" diff --git a/sinricpro/devices/sinric_pro_fan.py b/sinricpro/devices/sinric_pro_fan.py index 584ca81..7f4c374 100644 --- a/sinricpro/devices/sinric_pro_fan.py +++ b/sinricpro/devices/sinric_pro_fan.py @@ -25,18 +25,20 @@ async def handle_request(self, request: SinricProRequest) -> bool: return success elif request.action == ACTION_SET_RANGE_VALUE: range_value = request.request_value.get("rangeValue", 0) - success, response_value = await self.handle_range_value_request(range_value, self) + instance_id = request.instance + success, response_value = await self.handle_range_value_request(range_value, instance_id, self) request.response_value = response_value return success elif request.action == ACTION_ADJUST_RANGE_VALUE: range_value_delta = request.request_value.get("rangeValueDelta", 0) - success, response_value = await self.handle_adjust_range_value_request(range_value_delta, self) + instance_id = request.instance + success, response_value = await self.handle_adjust_range_value_request(range_value_delta, instance_id, self) request.response_value = response_value return success elif request.action == ACTION_SET_SETTING: - setting = request.request_value.get("setting", "") + setting_id = request.request_value.get("id", "") value = request.request_value.get("value") - success, response_value = await self.handle_setting_request(setting, value, self) + success, response_value = await self.handle_setting_request(setting_id, value, self) request.response_value = response_value return success request.error_message = f"Missing callback function: {request.action}" diff --git a/sinricpro/devices/sinric_pro_garage_door.py b/sinricpro/devices/sinric_pro_garage_door.py index a66b079..e4b9f57 100644 --- a/sinricpro/devices/sinric_pro_garage_door.py +++ b/sinricpro/devices/sinric_pro_garage_door.py @@ -13,13 +13,14 @@ def __init__(self, device_id: str) -> None: async def handle_request(self, request: SinricProRequest) -> bool: if request.action == ACTION_SET_MODE: state = request.request_value.get("mode", "Close") - success, response_value = await self.handle_mode_request(state, self) + instance_id = request.instance + success, response_value = await self.handle_mode_request(state, instance_id, self) request.response_value = response_value return success elif request.action == ACTION_SET_SETTING: - setting = request.request_value.get("setting", "") + setting_id = request.request_value.get("id", "") value = request.request_value.get("value") - success, response_value = await self.handle_setting_request(setting, value, self) + success, response_value = await self.handle_setting_request(setting_id, value, self) request.response_value = response_value return success request.error_message = f"Missing callback function: {request.action}" diff --git a/sinricpro/devices/sinric_pro_light.py b/sinricpro/devices/sinric_pro_light.py index 41f5e56..688c4dc 100644 --- a/sinricpro/devices/sinric_pro_light.py +++ b/sinricpro/devices/sinric_pro_light.py @@ -156,9 +156,9 @@ async def handle_request(self, request: SinricProRequest) -> bool: # Handle setSetting action elif action == ACTION_SET_SETTING: - setting = request.request_value.get("setting", "") + setting_id = request.request_value.get("id", "") value = request.request_value.get("value") - success, response_value = await self.handle_setting_request(setting, value, self) + success, response_value = await self.handle_setting_request(setting_id, value, self) request.response_value = response_value return success diff --git a/sinricpro/devices/sinric_pro_lock.py b/sinricpro/devices/sinric_pro_lock.py index 818abfe..dc78713 100644 --- a/sinricpro/devices/sinric_pro_lock.py +++ b/sinricpro/devices/sinric_pro_lock.py @@ -18,9 +18,9 @@ async def handle_request(self, request: SinricProRequest) -> bool: request.response_value = response_value return success elif request.action == ACTION_SET_SETTING: - setting = request.request_value.get("setting", "") + setting_id = request.request_value.get("id", "") value = request.request_value.get("value") - success, response_value = await self.handle_setting_request(setting, value, self) + success, response_value = await self.handle_setting_request(setting_id, value, self) request.response_value = response_value return success request.error_message = f"Missing callback function: {request.action}" diff --git a/sinricpro/devices/sinric_pro_motion_sensor.py b/sinricpro/devices/sinric_pro_motion_sensor.py index 864d06c..ecf3c23 100644 --- a/sinricpro/devices/sinric_pro_motion_sensor.py +++ b/sinricpro/devices/sinric_pro_motion_sensor.py @@ -12,9 +12,9 @@ def __init__(self, device_id: str) -> None: super().__init__(device_id=device_id, product_type="MOTION_SENSOR") async def handle_request(self, request: SinricProRequest) -> bool: if request.action == ACTION_SET_SETTING: - setting = request.request_value.get("setting", "") + setting_id = request.request_value.get("id", "") value = request.request_value.get("value") - success, response_value = await self.handle_setting_request(setting, value, self) + success, response_value = await self.handle_setting_request(setting_id, value, self) request.response_value = response_value return success request.error_message = f"Missing callback function: {request.action}" diff --git a/sinricpro/devices/sinric_pro_power_sensor.py b/sinricpro/devices/sinric_pro_power_sensor.py index 71077cc..8148454 100644 --- a/sinricpro/devices/sinric_pro_power_sensor.py +++ b/sinricpro/devices/sinric_pro_power_sensor.py @@ -12,9 +12,9 @@ def __init__(self, device_id: str) -> None: super().__init__(device_id=device_id, product_type="POWER_SENSOR") async def handle_request(self, request: SinricProRequest) -> bool: if request.action == ACTION_SET_SETTING: - setting = request.request_value.get("setting", "") + setting_id = request.request_value.get("id", "") value = request.request_value.get("value") - success, response_value = await self.handle_setting_request(setting, value, self) + success, response_value = await self.handle_setting_request(setting_id, value, self) request.response_value = response_value return success request.error_message = f"Missing callback function: {request.action}" diff --git a/sinricpro/devices/sinric_pro_speaker.py b/sinricpro/devices/sinric_pro_speaker.py new file mode 100644 index 0000000..64fb88d --- /dev/null +++ b/sinricpro/devices/sinric_pro_speaker.py @@ -0,0 +1,162 @@ +""" +SinricProSpeaker Device + +Smart speaker device with power, volume, mute, media, equalizer, and mode control. +""" + +from sinricpro.capabilities.power_state_controller import PowerStateController +from sinricpro.capabilities.volume_controller import VolumeController +from sinricpro.capabilities.mute_controller import MuteController +from sinricpro.capabilities.media_controller import MediaController +from sinricpro.capabilities.equalizer_controller import EqualizerController +from sinricpro.capabilities.mode_controller import ModeController +from sinricpro.capabilities.push_notification import PushNotification +from sinricpro.capabilities.setting_controller import SettingController +from sinricpro.core.actions import ( + ACTION_SET_POWER_STATE, + ACTION_SET_VOLUME, + ACTION_ADJUST_VOLUME, + ACTION_SET_MUTE, + ACTION_MEDIA_CONTROL, + ACTION_SET_BANDS, + ACTION_ADJUST_BANDS, + ACTION_SET_MODE, + ACTION_SET_SETTING, +) +from sinricpro.core.sinric_pro_device import SinricProDevice +from sinricpro.core.types import SinricProRequest + + +class SinricProSpeaker( + SinricProDevice, + PowerStateController, + VolumeController, + MuteController, + MediaController, + EqualizerController, + ModeController, + SettingController, + PushNotification +): + """ + SinricPro Speaker device. + + A smart speaker with power, volume, mute, media playback, equalizer, and mode control. + + Example: + >>> from sinricpro import SinricPro, SinricProSpeaker + >>> + >>> # Create Speaker + >>> my_speaker = SinricProSpeaker("5dc1564130xxxxxxxxxxxxxx") + >>> + >>> # Register power state callback + >>> async def on_power_state(state: bool) -> bool: + ... print(f"Speaker {'on' if state else 'off'}") + ... return True + >>> + >>> # Register volume callback + >>> async def on_volume(volume: int) -> bool: + ... print(f"Volume set to {volume}") + ... return True + >>> + >>> my_speaker.on_power_state(on_power_state) + >>> my_speaker.on_volume(on_volume) + >>> + >>> # Add to SinricPro + >>> sinric_pro = SinricPro.get_instance() + >>> sinric_pro.add(my_speaker) + """ + + def __init__(self, device_id: str) -> None: + """ + Initialize a SinricProSpeaker. + + Args: + device_id: Unique device ID (24 hex characters) + + Example: + >>> my_speaker = SinricProSpeaker("5dc1564130xxxxxxxxxxxxxx") + """ + super().__init__(device_id=device_id, product_type="SPEAKER") + + async def handle_request(self, request: SinricProRequest) -> bool: + """ + Handle incoming requests for this speaker. + + Args: + request: The request to handle + + Returns: + True if request was handled successfully, False otherwise + """ + action = request.action + + # Handle setPowerState action + if action == ACTION_SET_POWER_STATE: + state_str = request.request_value.get("state", "Off") + state = state_str.lower() == "on" + success, response_value = await self.handle_power_state_request(state, self) + request.response_value = response_value + return success + + # Handle setVolume action + elif action == ACTION_SET_VOLUME: + volume = request.request_value.get("volume", 0) + success, response_value = await self.handle_volume_request(volume, self) + request.response_value = response_value + return success + + # Handle adjustVolume action + elif action == ACTION_ADJUST_VOLUME: + volume_delta = request.request_value.get("volumeDelta", 0) + success, response_value = await self.handle_adjust_volume_request(volume_delta, self) + request.response_value = response_value + return success + + # Handle setMute action + elif action == ACTION_SET_MUTE: + mute = request.request_value.get("mute", False) + success, response_value = await self.handle_mute_request(mute, self) + request.response_value = response_value + return success + + # Handle mediaControl action + elif action == ACTION_MEDIA_CONTROL: + control = request.request_value.get("control", "") + success, response_value = await self.handle_media_control_request(control, self) + request.response_value = response_value + return success + + # Handle setBands action + elif action == ACTION_SET_BANDS: + bands = request.request_value.get("bands", {}) + success, response_value = await self.handle_set_bands_request(bands, self) + request.response_value = response_value + return success + + # Handle adjustBands action + elif action == ACTION_ADJUST_BANDS: + bands = request.request_value.get("bands", {}) + success, response_value = await self.handle_adjust_bands_request(bands, self) + request.response_value = response_value + return success + + # Handle setMode action + elif action == ACTION_SET_MODE: + mode = request.request_value.get("mode", "") + instance_id = request.instance + success, response_value = await self.handle_mode_request(mode, instance_id, self) + request.response_value = response_value + return success + + # Handle setSetting action + elif action == ACTION_SET_SETTING: + setting_id = request.request_value.get("id", "") + value = request.request_value.get("value") + success, response_value = await self.handle_setting_request(setting_id, value, self) + request.response_value = response_value + return success + + # Missing callback function + request.error_message = f"Missing callback function: {action}" + return False diff --git a/sinricpro/devices/sinric_pro_switch.py b/sinricpro/devices/sinric_pro_switch.py index 94957e9..b845dbb 100644 --- a/sinricpro/devices/sinric_pro_switch.py +++ b/sinricpro/devices/sinric_pro_switch.py @@ -74,9 +74,9 @@ async def handle_request(self, request: SinricProRequest) -> bool: # Handle setSetting action elif action == ACTION_SET_SETTING: - setting = request.request_value.get("setting", "") + setting_id = request.request_value.get("id", "") value = request.request_value.get("value") - success, response_value = await self.handle_setting_request(setting, value, self) + success, response_value = await self.handle_setting_request(setting_id, value, self) request.response_value = response_value return success diff --git a/sinricpro/devices/sinric_pro_temperature_sensor.py b/sinricpro/devices/sinric_pro_temperature_sensor.py index 1292d14..1245f78 100644 --- a/sinricpro/devices/sinric_pro_temperature_sensor.py +++ b/sinricpro/devices/sinric_pro_temperature_sensor.py @@ -12,9 +12,9 @@ def __init__(self, device_id: str) -> None: super().__init__(device_id=device_id, product_type="TEMPERATURE_SENSOR") async def handle_request(self, request: SinricProRequest) -> bool: if request.action == ACTION_SET_SETTING: - setting = request.request_value.get("setting", "") + setting_id = request.request_value.get("id", "") value = request.request_value.get("value") - success, response_value = await self.handle_setting_request(setting, value, self) + success, response_value = await self.handle_setting_request(setting_id, value, self) request.response_value = response_value return success request.error_message = f"Missing callback function: {request.action}" diff --git a/sinricpro/devices/sinric_pro_thermostat.py b/sinricpro/devices/sinric_pro_thermostat.py index a4e752b..670a7d0 100644 --- a/sinricpro/devices/sinric_pro_thermostat.py +++ b/sinricpro/devices/sinric_pro_thermostat.py @@ -35,9 +35,9 @@ async def handle_request(self, request: SinricProRequest) -> bool: request.response_value = response_value return success elif request.action == ACTION_SET_SETTING: - setting = request.request_value.get("setting", "") + setting_id = request.request_value.get("id", "") value = request.request_value.get("value") - success, response_value = await self.handle_setting_request(setting, value, self) + success, response_value = await self.handle_setting_request(setting_id, value, self) request.response_value = response_value return success request.error_message = f"Missing callback function: {request.action}" diff --git a/sinricpro/devices/sinric_pro_tv.py b/sinricpro/devices/sinric_pro_tv.py new file mode 100644 index 0000000..c3b7716 --- /dev/null +++ b/sinricpro/devices/sinric_pro_tv.py @@ -0,0 +1,167 @@ +""" +SinricProTV Device + +Smart TV device with power, volume, mute, media, channel, and input control. +""" + +from sinricpro.capabilities.power_state_controller import PowerStateController +from sinricpro.capabilities.volume_controller import VolumeController +from sinricpro.capabilities.mute_controller import MuteController +from sinricpro.capabilities.media_controller import MediaController +from sinricpro.capabilities.channel_controller import ChannelController +from sinricpro.capabilities.input_controller import InputController +from sinricpro.capabilities.push_notification import PushNotification +from sinricpro.capabilities.setting_controller import SettingController +from sinricpro.core.actions import ( + ACTION_SET_POWER_STATE, + ACTION_SET_VOLUME, + ACTION_ADJUST_VOLUME, + ACTION_SET_MUTE, + ACTION_MEDIA_CONTROL, + ACTION_CHANGE_CHANNEL, + ACTION_SKIP_CHANNELS, + ACTION_SELECT_INPUT, + ACTION_SET_SETTING, +) +from sinricpro.core.sinric_pro_device import SinricProDevice +from sinricpro.core.types import SinricProRequest + + +class SinricProTV( + SinricProDevice, + PowerStateController, + VolumeController, + MuteController, + MediaController, + ChannelController, + InputController, + SettingController, + PushNotification +): + """ + SinricPro TV device. + + A smart TV with power, volume, mute, media playback, channel, and input control. + + Example: + >>> from sinricpro import SinricPro, SinricProTV + >>> + >>> # Create TV + >>> my_tv = SinricProTV("5dc1564130xxxxxxxxxxxxxx") + >>> + >>> # Register power state callback + >>> async def on_power_state(state: bool) -> bool: + ... print(f"TV {'on' if state else 'off'}") + ... return True + >>> + >>> # Register volume callback + >>> async def on_volume(volume: int) -> bool: + ... print(f"Volume set to {volume}") + ... return True + >>> + >>> # Register channel callback + >>> async def on_change_channel(channel: dict) -> bool: + ... print(f"Channel changed to {channel.get('name') or channel.get('number')}") + ... return True + >>> + >>> my_tv.on_power_state(on_power_state) + >>> my_tv.on_volume(on_volume) + >>> my_tv.on_change_channel(on_change_channel) + >>> + >>> # Add to SinricPro + >>> sinric_pro = SinricPro.get_instance() + >>> sinric_pro.add(my_tv) + """ + + def __init__(self, device_id: str) -> None: + """ + Initialize a SinricProTV. + + Args: + device_id: Unique device ID (24 hex characters) + + Example: + >>> my_tv = SinricProTV("5dc1564130xxxxxxxxxxxxxx") + """ + super().__init__(device_id=device_id, product_type="TV") + + async def handle_request(self, request: SinricProRequest) -> bool: + """ + Handle incoming requests for this TV. + + Args: + request: The request to handle + + Returns: + True if request was handled successfully, False otherwise + """ + action = request.action + + # Handle setPowerState action + if action == ACTION_SET_POWER_STATE: + state_str = request.request_value.get("state", "Off") + state = state_str.lower() == "on" + success, response_value = await self.handle_power_state_request(state, self) + request.response_value = response_value + return success + + # Handle setVolume action + elif action == ACTION_SET_VOLUME: + volume = request.request_value.get("volume", 0) + success, response_value = await self.handle_volume_request(volume, self) + request.response_value = response_value + return success + + # Handle adjustVolume action + elif action == ACTION_ADJUST_VOLUME: + volume_delta = request.request_value.get("volumeDelta", 0) + success, response_value = await self.handle_adjust_volume_request(volume_delta, self) + request.response_value = response_value + return success + + # Handle setMute action + elif action == ACTION_SET_MUTE: + mute = request.request_value.get("mute", False) + success, response_value = await self.handle_mute_request(mute, self) + request.response_value = response_value + return success + + # Handle mediaControl action + elif action == ACTION_MEDIA_CONTROL: + control = request.request_value.get("control", "") + success, response_value = await self.handle_media_control_request(control, self) + request.response_value = response_value + return success + + # Handle changeChannel action + elif action == ACTION_CHANGE_CHANNEL: + channel = request.request_value.get("channel", {}) + success, response_value = await self.handle_change_channel_request(channel, self) + request.response_value = response_value + return success + + # Handle skipChannels action + elif action == ACTION_SKIP_CHANNELS: + channel_count = request.request_value.get("channelCount", 0) + success, response_value = await self.handle_skip_channels_request(channel_count, self) + request.response_value = response_value + return success + + # Handle selectInput action + elif action == ACTION_SELECT_INPUT: + input_name = request.request_value.get("input", "") + success, response_value = await self.handle_select_input_request(input_name, self) + request.response_value = response_value + return success + + # Handle setSetting action + elif action == ACTION_SET_SETTING: + setting_id = request.request_value.get("id", "") + value = request.request_value.get("value") + success, response_value = await self.handle_setting_request(setting_id, value, self) + request.response_value = response_value + return success + + # Missing callback function + request.error_message = f"Missing callback function: {action}" + return False diff --git a/sinricpro/devices/sinric_pro_window_ac.py b/sinricpro/devices/sinric_pro_window_ac.py index 7c094e4..05112e2 100644 --- a/sinricpro/devices/sinric_pro_window_ac.py +++ b/sinricpro/devices/sinric_pro_window_ac.py @@ -39,18 +39,20 @@ async def handle_request(self, request: SinricProRequest) -> bool: return success elif request.action == ACTION_SET_RANGE_VALUE: range_value = request.request_value.get("rangeValue", 0) - success, response_value = await self.handle_range_value_request(range_value, self) + instance_id = request.instance + success, response_value = await self.handle_range_value_request(range_value, instance_id, self) request.response_value = response_value return success elif request.action == ACTION_ADJUST_RANGE_VALUE: range_value_delta = request.request_value.get("rangeValueDelta", 0) - success, response_value = await self.handle_adjust_range_value_request(range_value_delta, self) + instance_id = request.instance + success, response_value = await self.handle_adjust_range_value_request(range_value_delta, instance_id, self) request.response_value = response_value return success elif request.action == ACTION_SET_SETTING: - setting = request.request_value.get("setting", "") + setting_id = request.request_value.get("id", "") value = request.request_value.get("value") - success, response_value = await self.handle_setting_request(setting, value, self) + success, response_value = await self.handle_setting_request(setting_id, value, self) request.response_value = response_value return success request.error_message = f"Missing callback function: {request.action}"