From fee544e8b1cac2d02dcf1d3f62e1c127fdfd1cf6 Mon Sep 17 00:00:00 2001 From: zim514 Date: Sun, 24 Dec 2023 16:57:15 -0500 Subject: [PATCH] Finish discovery Improve core loop execution Improve Timers execution --- .../resource.language.en_gb/strings.po | 7 ++ script.service.hue/resources/lib/core.py | 54 ++++++---- script.service.hue/resources/lib/huev2.py | 99 +++++++++++-------- script.service.hue/resources/lib/language.py | 4 +- .../resources/lib/lightgroup.py | 11 ++- 5 files changed, 107 insertions(+), 68 deletions(-) diff --git a/script.service.hue/resources/language/resource.language.en_gb/strings.po b/script.service.hue/resources/language/resource.language.en_gb/strings.po index f5d8006f..67017e91 100644 --- a/script.service.hue/resources/language/resource.language.en_gb/strings.po +++ b/script.service.hue/resources/language/resource.language.en_gb/strings.po @@ -402,6 +402,13 @@ msgctxt "#30083" msgid "Unknown" msgstr "" +msgctxt "#30084" +msgid "User Found![CR]Saving settings..." +msgstr "" + +msgctxt "#30086" +msgid "User not found[CR]Check your bridge and network." +msgstr "" msgctxt "#30073" msgid "Do not show again" diff --git a/script.service.hue/resources/lib/core.py b/script.service.hue/resources/lib/core.py index ccee0fcf..ae8bff04 100644 --- a/script.service.hue/resources/lib/core.py +++ b/script.service.hue/resources/lib/core.py @@ -52,13 +52,13 @@ def handle_command(self, command, *args): raise RuntimeError(f"Unknown Command: {command}") def discover(self): - hue_connection = HueConnection(self.monitor, silent=True, discover=True) - if hue_connection.connected: - xbmc.log("[script.service.hue] Found bridge. Starting service.") + bridge = HueAPIv2(self.monitor, discover=True) + if bridge.connected: + xbmc.log("[script.service.hue] Found bridge. Opening settings.") ADDON.openSettings() - service = HueService(self.monitor) - service.run() + else: + xbmc.log("[script.service.hue] No bridge found. Opening settings.") ADDON.openSettings() def scene_select(self, light_group, action): @@ -82,7 +82,7 @@ def ambi_light_select(self, light_group): class HueService: def __init__(self, monitor): self.monitor = monitor - self.hue_connection = HueConnection(monitor, discover=False) + # self.hue_connection = None # HueConnection(monitor, discover=False) self.bridge = HueAPIv2(monitor) self.light_groups = [] self.timers = None @@ -90,14 +90,12 @@ def __init__(self, monitor): cache_set("service_enabled", True) def run(self): - if not (self.bridge.connected and self.hue_connection.connected): - xbmc.log("[script.service.hue] Not connected, exiting...") - return - # Initialize light groups and timers only if the connection is successful self.light_groups = self.initialize_light_groups() self.timers = Timers(self.monitor, self.bridge, self) - self.timers.start() + + if self.bridge.connected: + self.timers.start() # Track the previous state of the service prev_service_enabled = self.service_enabled @@ -107,29 +105,37 @@ def run(self): self.service_enabled = cache_get("service_enabled") if self.service_enabled: + self._reload_settings_if_needed() + self._process_actions() + # If the service was previously disabled and is now enabled, activate light groups - if not prev_service_enabled: + if not prev_service_enabled and self.bridge.connected: self.activate() - - self._process_actions() - self._reload_settings_if_needed() else: AMBI_RUNNING.clear() + # If the bridge gets disconnected, stop the timers + if not self.bridge.connected and self.timers.is_alive(): + self.timers.stop() + + # If the bridge gets reconnected, restart the timers + if self.bridge.connected and not self.timers.is_alive(): + self.timers.start() + # Update the previous state for the next iteration prev_service_enabled = self.service_enabled if self.monitor.waitForAbort(1): break - xbmc.log("[script.service.hue] Process exiting...") + xbmc.log("[script.service.hue] Abort requested...") def initialize_light_groups(self): # Initialize light groups return [ lightgroup.LightGroup(0, self.bridge, lightgroup.VIDEO), lightgroup.LightGroup(1, self.bridge, lightgroup.AUDIO) - #ambigroup.AmbiGroup(3, self.hue_connection) + # ambigroup.AmbiGroup(3, self.hue_connection) ] def activate(self): @@ -167,7 +173,8 @@ def _reload_settings_if_needed(self): self.bridge.reload_settings() # If IP or key has changed, attempt to reconnect - if old_ip != self.bridge.ip or old_key != self.bridge.key: + if (old_ip != self.bridge.ip or old_key != self.bridge.key) and self.bridge.ip and self.bridge.key: + xbmc.log("[script.service.hue] IP or key changed, reconnecting...") self.bridge.connect() SETTINGS_CHANGED.clear() @@ -208,12 +215,18 @@ def __init__(self, monitor, bridge, hue_service): self.bridge = bridge self.hue_service = hue_service self.morning_time = datetime.datetime.strptime(ADDON.getSettingString("morningTime"), "%H:%M").time() - self._set_daytime() + + self.stop_timers = threading.Event() # Flag to stop the thread + super().__init__() def run(self): + self._set_daytime() self._task_loop() + def stop(self): + self.stop_timers.set() + def _run_morning(self): cache_set("daytime", True) self.bridge.update_sunset() @@ -234,7 +247,7 @@ def _set_daytime(self): def _task_loop(self): - while not self.monitor.abortRequested(): + while not self.monitor.abortRequested() and not self.stop_timers.is_set(): now = datetime.datetime.now() self.morning_time = datetime.datetime.strptime(ADDON.getSettingString("morningTime"), "%H:%M").time() @@ -257,6 +270,7 @@ def _task_loop(self): if self.monitor.waitForAbort(wait_time): break self._run_morning() + xbmc.log("[script.service.hue] Timers stopped") @staticmethod def _time_until(current, target): diff --git a/script.service.hue/resources/lib/huev2.py b/script.service.hue/resources/lib/huev2.py index f1f96eed..415bdd45 100644 --- a/script.service.hue/resources/lib/huev2.py +++ b/script.service.hue/resources/lib/huev2.py @@ -38,12 +38,14 @@ def __init__(self, monitor, discover=False): self.monitor = monitor self.reload_settings() - if self.ip is not None and self.key is not None: - self.connected = self.connect() - elif self.discover: + xbmc.log(f"[script.service.hue] v2 init: ip: {type(self.ip)}, key: {type(self.key)}") + if discover: self.discover() + elif self.ip != "" or self.key != "": + self.connected = self.connect() else: - raise ValueError("ip and key must be provided or discover must be True") + xbmc.log("[script.service.hue] No bridge IP or user key provided. Bridge not configured.") + notification(_("Hue Service"), _("Bridge not configured. Please check your settings."), icon=xbmcgui.NOTIFICATION_ERROR) def reload_settings(self): self.ip = ADDON.getSetting("bridgeIP") @@ -52,42 +54,48 @@ def reload_settings(self): def __exit__(self, exc_type, exc_val, exc_tb): self.session.close() - def make_api_request(self, method, resource, v1=False, **kwargs): + def make_api_request(self, method, resource, discovery=False, **kwargs): + # Discovery and account creation not yet supported on API V2. This flag uses a V1 URL and supports new IPs. + if discovery: + xbmc.log(f"[script.service.hue] v2 make_request: discovery mode") for attempt in range(MAX_RETRIES): # Prepare the URL for the request - base_url = self.base_url if not v1 else f"http://{self.ip}/api" + xbmc.log(f"[script.service.hue] v2 ip: {self.ip}, key: {self.key}") + base_url = self.base_url if not discovery else f"http://{self.ip}/api/" url = urljoin(base_url, resource) - xbmc.log(f"[script.service.hue] v2 make_request: url: {url}, method: {method}, kwargs: {kwargs}") + xbmc.log(f"[script.service.hue] v2 make_request: base_url: {base_url}, url: {url}, method: {method}, kwargs: {kwargs}") try: # Make the request response = self.session.request(method, url, timeout=TIMEOUT, **kwargs) response.raise_for_status() return response.json() except ConnectionError as x: - # If a ConnectionError occurs, try to discover a new IP + # If a ConnectionError occurs, try to handle a new IP, except in discovery mode xbmc.log(f"[script.service.hue] v2 make_request: ConnectionError: {x}") - if self._discover_bridge_ip(): - # If a new IP is found, update the IP and retry the request - xbmc.log(f"[script.service.hue] v2 make_request: New IP found: {self.bridge_ip}. Retrying request.") - self.ip = self.bridge_ip - ADDON.setSettingString("bridgeIP", self.bridge_ip) + if self._discover_new_ip() and not discovery: + # If handling a new IP is successful, retry the request + xbmc.log(f"[script.service.hue] v2 make_request: New IP handled successfully. Retrying request.") continue else: - # If a new IP is not found, abort the request - xbmc.log(f"[script.service.hue] v2 make_request: Failed to discover new IP. Aborting request.") - break + # If handling a new IP fails, abort the request + xbmc.log(f"[script.service.hue] v2 make_request: Failed to handle new IP. Aborting request.") + return None + except HTTPError as x: # Handle HTTP errors if x.response.status_code == 429: # If a 429 status code is received, abort and log an error xbmc.log(f"[script.service.hue] v2 make_request: Too Many Requests: {x}. Aborting request.") notification(_("Hue Service"), _("Bridge not found. Please check your network or enter IP manually."), icon=xbmcgui.NOTIFICATION_ERROR) - break + return 429 elif x.response.status_code in [401, 403]: xbmc.log(f"[script.service.hue] v2 make_request: Unauthorized: {x}") notification(_("Hue Service"), _("Bridge unauthorized, please reconfigure."), icon=xbmcgui.NOTIFICATION_ERROR) ADDON.setSettingString("bridgeUser", "") return None + elif x.response.status_code == 404: + xbmc.log(f"[script.service.hue] v2 make_request: Not Found: {x}") + return 404 else: xbmc.log(f"[script.service.hue] v2 make_request: HTTPError: {x}") except (Timeout, json.JSONDecodeError) as x: @@ -107,6 +115,17 @@ def make_api_request(self, method, resource, v1=False, **kwargs): self.connected = False return None + def _discover_new_ip(self): + if self._discover_nupnp(): + xbmc.log(f"[script.service.hue] v2 _discover_and_handle_new_ip: discover_nupnp SUCCESS, bridge IP: {self.ip}") + self.ip = self.bridge_ip + ADDON.setSettingString("bridgeIP", self.ip) + if self.connect(): + xbmc.log(f"[script.service.hue] v2 _discover_and_handle_new_ip: connect SUCCESS") + return True + xbmc.log(f"[script.service.hue] v2 _discover_and_handle_new_ip: discover_nupnp FAIL, bridge IP: {self.ip}") + return False + def connect(self): xbmc.log(f"[script.service.hue] v2 connect: ip: {self.ip}, key: {self.key}") self.base_url = f"https://{self.ip}/clip/v2/resource/" @@ -132,13 +151,13 @@ def connect(self): def discover(self): xbmc.log("[script.service.hue] v2 Start discover") # Reset settings - ADDON.setSettingString("bridgeIP", "") - ADDON.setSettingString("bridgeUser", "") self.ip = "" self.key = "" - self.connected = False + ADDON.setSettingString("bridgeIP", "") + ADDON.setSettingString("bridgeUser", "") + progress_bar = xbmcgui.DialogProgress() progress_bar.create(_('Searching for bridge...')) progress_bar.update(5, _("Discovery started")) @@ -148,23 +167,27 @@ def discover(self): progress_bar.update(percent=10, message=_("N-UPnP discovery...")) # Try to discover the bridge using N-UPnP - bridge_ip_found = self._discover_nupnp() + ip_discovered = self._discover_nupnp() - if not bridge_ip_found and not progress_bar.iscanceled(): + if not ip_discovered and not progress_bar.iscanceled(): # If the bridge was not found, ask the user to enter the IP manually - manual_entry = xbmcgui.Dialog().yesno(_("Bridge not found"), - _("Bridge not found automatically. Please make sure your bridge is up to date and has access to the internet. [CR]Would you like to enter your bridge IP manually?") + xbmc.log("[script.service.hue] v2 discover: Bridge not found automatically") + progress_bar.update(percent=10, message=_("Bridge not found")) + manual_entry = xbmcgui.Dialog().yesno(_("Bridge not found"), _("Bridge not found automatically. Please make sure your bridge is up to date and has access to the internet. [CR]Would you like to enter your bridge IP manually?") ) if manual_entry: self.ip = xbmcgui.Dialog().numeric(3, _("Bridge IP")) + xbmc.log(f"[script.service.hue] v2 discover: Manual entry: {self.ip}") if self.ip: progress_bar.update(percent=50, message=_("Connecting...")) # Set the base URL for the API self.base_url = f"https://{self.ip}/clip/v2/resource/" # Try to connect to the bridge - self.devices = self.make_api_request("GET", "device") - if self.devices is not None and not progress_bar.iscanceled(): + xbmc.log(f"[script.service.hue] v2 discover: Attempt connection") + config = self.make_api_request("GET", "0/config", discovery=True) # bypass some checks in discovery mode, and use Hue API V1 until Philipps provides a V2 method + xbmc.log(f"[script.service.hue] v2 discover: config: {config}") + if config is not None and isinstance(config, dict) and not progress_bar.iscanceled(): progress_bar.update(percent=100, message=_("Found bridge: ") + self.ip) self.monitor.waitForAbort(1) @@ -184,7 +207,7 @@ def discover(self): progress_bar.close() xbmc.log("[script.service.hue] v2 discover: Bridge discovery complete") self.connect() - return True + return elif progress_bar.iscanceled(): xbmc.log("[script.service.hue] v2 discover: Discovery cancelled by user") @@ -239,10 +262,11 @@ def _create_user(self, progress_bar): progress_bar.update(percent=progress, message=_("Press link button on bridge. Waiting for 90 seconds...")) last_progress = progress - res = self.make_api_request("POST", "", v1=True, data=data) + response = self.make_api_request("POST", "", discovery=True, data=data) + xbmc.log(f"[script.service.hue] v2 _create_user: response at iteration {time}: {response}") # Break loop if link button has been pressed - if res and 'link button not pressed' not in res[0]: + if response and response[0].get('error', {}).get('type') != 101: break self.monitor.waitForAbort(1) @@ -253,8 +277,9 @@ def _create_user(self, progress_bar): try: # Extract and save username from response - username = res[0]['success']['username'] - self.bridge_user = username + username = response[0]['success']['username'] + self.key = username + xbmc.log(f"[script.service.hue] v2 _create_user: User created: {username}") return True except (KeyError, TypeError) as exc: xbmc.log(f"[script.service.hue] v2 _create_user: Username not found: {exc}") @@ -367,16 +392,6 @@ def select_hue_scene(self): dialog_progress.close() return None - def _discover_bridge_ip(self): - xbmc.log("[script.service.hue] v2 _discover_bridge_ip:") - if self._discover_nupnp(): - xbmc.log(f"[script.service.hue] v2 _discover_bridge_ip: discover_nupnp SUCCESS, bridge IP: {self.bridge_ip}") - if self._check_version(): - xbmc.log(f"[script.service.hue] v2 _discover_bridge_ip: check version SUCCESS") - return True - xbmc.log(f"[script.service.hue] v2 _discover_bridge_ip: discover_nupnp FAIL, bridge IP: {self.bridge_ip}") - return False - def _discover_nupnp(self): xbmc.log("[script.service.hue] v2 _discover_nupnp:") result = self.make_api_request('GET', 'https://discovery.meethue.com/') @@ -391,7 +406,7 @@ def _discover_nupnp(self): except KeyError: xbmc.log("[script.service.hue] v2 _discover_nupnp: No IP found in response") return None - self.bridge_ip = bridge_ip + self.ip = bridge_ip return True @staticmethod diff --git a/script.service.hue/resources/lib/language.py b/script.service.hue/resources/lib/language.py index cd134362..58f1fff8 100644 --- a/script.service.hue/resources/lib/language.py +++ b/script.service.hue/resources/lib/language.py @@ -122,7 +122,7 @@ def get_string(t): _strings['unknown'] = 30083 _strings['do not show again'] = 30073 _strings['disable hue labs during playback'] = 30074 -_strings['hue bridge v1 (round) is unsupported. hue bridge v2 (square) is required.'] = 30001 +_strings['hue bridge discovery (round) is unsupported. hue bridge v2 (square) is required.'] = 30001 _strings['unknown colour gamut for light:'] = 30012 _strings['report errors'] = 30016 _strings['never report errors'] = 30020 @@ -136,3 +136,5 @@ def get_string(t): _strings['reconnected'] = 30033 _strings['re-enable time'] = 31334 _strings['transition time (seconds):'] = 31335 +_strings['user found![cr]saving settings...'] = 30084 +_strings['user not found[cr]check your bridge and network.'] = 30086 diff --git a/script.service.hue/resources/lib/lightgroup.py b/script.service.hue/resources/lib/lightgroup.py index c155f5b2..72cdd79d 100644 --- a/script.service.hue/resources/lib/lightgroup.py +++ b/script.service.hue/resources/lib/lightgroup.py @@ -69,7 +69,7 @@ def fetch_all_light_states(self): return self.bridge.make_api_request("GET", "lights") def onAVStarted(self): - if not self.enabled: + if not self.enabled or not self.bridge.connected: return xbmc.log(f"[script.service.hue] In LightGroup[{self.light_group_id}], onPlaybackStarted. Group enabled: {self.enabled}, startBehavior: {self.play_behavior}, isPlayingVideo: {self.isPlayingVideo()}, isPlayingAudio: {self.isPlayingAudio()}, self.mediaType: {self.media_type}, self.playbackType(): {self.playback_type()}") @@ -85,7 +85,7 @@ def onAVStarted(self): self.run_action("play") def onPlayBackPaused(self): - if not self.enabled: + if not self.enabled or not self.bridge.connected: return xbmc.log(f"[script.service.hue] In LightGroup[{self.light_group_id}], onPlaybackPaused()") @@ -100,7 +100,7 @@ def onPlayBackPaused(self): self.run_action("pause") def onPlayBackStopped(self): - if not self.enabled: + if not self.enabled or not self.bridge.connected: return xbmc.log(f"[script.service.hue] In LightGroup[{self.light_group_id}], onPlaybackStopped()") @@ -127,9 +127,9 @@ def onPlayBackEnded(self): self.onPlayBackStopped() def run_action(self, action): - xbmc.log(f"[script.service.hue] In LightGroup[{self.light_group_id}], run_action({action})") + xbmc.log(f"[script.service.hue] LightGroup[{self.light_group_id}], run_action({action})") service_enabled = cache_get("service_enabled") - if service_enabled: + if service_enabled and self.bridge.connected: if action == "play": scene = self.play_scene duration = self.play_transition @@ -156,6 +156,7 @@ def run_action(self, action): except Exception as exc: reporting.process_exception(exc) + xbmc.log(f"[script.service.hue] LightGroup[{self.light_group_id}] run_action({action}), service_enabled: {service_enabled}, bridge_connected: {self.bridge.connected}") def activate(self): xbmc.log(f"[script.service.hue] Activate group [{self.light_group_id}]. State: {self.state}")