Skip to content

Commit

Permalink
Finish discovery
Browse files Browse the repository at this point in the history
Improve core loop execution
Improve Timers execution
  • Loading branch information
zim514 committed Dec 24, 2023
1 parent 88509ff commit fee544e
Show file tree
Hide file tree
Showing 5 changed files with 107 additions and 68 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
54 changes: 34 additions & 20 deletions script.service.hue/resources/lib/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand All @@ -82,22 +82,20 @@ 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
self.service_enabled = True
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
Expand All @@ -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):
Expand Down Expand Up @@ -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()

Expand Down Expand Up @@ -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()
Expand All @@ -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()
Expand All @@ -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):
Expand Down
99 changes: 57 additions & 42 deletions script.service.hue/resources/lib/huev2.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand All @@ -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:
Expand All @@ -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/"
Expand All @@ -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"))
Expand All @@ -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)

Expand All @@ -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")
Expand Down Expand Up @@ -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)
Expand All @@ -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}")
Expand Down Expand Up @@ -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/')
Expand All @@ -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
Expand Down
4 changes: 3 additions & 1 deletion script.service.hue/resources/lib/language.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Loading

0 comments on commit fee544e

Please sign in to comment.