Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improved exception handling and reconnection #9

Merged
merged 5 commits into from
Feb 16, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
168 changes: 48 additions & 120 deletions custom_components/jellyfish_lighting/api.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
"""Sample API Client."""
import logging
from typing import List, Dict
from threading import Lock
from typing import List
from homeassistant.core import HomeAssistant
from homeassistant.config_entries import ConfigEntry
import jellyfishlightspy as jf
Expand All @@ -19,51 +18,50 @@ def __init__(
self.host = host
self._config_entry = config_entry
self._hass = hass
self._reconnect = False
# two connections improves responsiveness for entity controls if actions are done during a scheduled update
self._interval_controller = JellyFishLightingController(host)
self._entity_controller = JellyFishLightingController(host)
self._controller = jf.JellyFishController(host, False)
self.zones = None
self.states = None
self.patterns = None

async def connect(self):
"""Establish connection to the controller"""
try:
if not self._controller.connected:
_LOGGER.debug(
"Connecting to the JellyFish Lighting controller at %s", self.host
)
await self._hass.async_add_executor_job(self._controller.connect)
except BaseException as ex: # pylint: disable=broad-except
msg = f"Failed to connect to JellyFish Lighting controller at {self.host}"
_LOGGER.exception(msg)
raise Exception(msg) from ex

async def async_get_data(self):
"""Get data from the API."""
await self.connect()
try:
_LOGGER.debug("Getting refreshed data for JellyFish Lighting")

# Get data
await self._hass.async_add_executor_job(
self._interval_controller.getAndStoreZones
)
await self._hass.async_add_executor_job(
self._interval_controller.getAndStorePatterns
# Get patterns
patterns = await self._hass.async_add_executor_job(
self._controller.getPatternList
)
self.patterns = [p.toFolderAndName() for p in patterns]
self.patterns.sort()
_LOGGER.debug("Patterns: %s", ", ".join(self.patterns))

# Get Zones
zones = await self._hass.async_add_executor_job(self._controller.getZones)

# Check if zones have changed
if self.zones is not None and set(self.zones) != set(
self._interval_controller.zones
):
if self.zones is not None and set(self.zones) != set(list(zones)):
# TODO: reload entities?
pass

# Get zones
self.zones = self._interval_controller.zones
self.zones = list(zones)
_LOGGER.debug("Zones: %s", ", ".join(self.zones))

# Get the list of available patterns/effects
self.patterns = list(
set(
[
p.toFolderAndName()
for p in self._interval_controller.patternFiles
]
)
)
self.patterns.sort()
_LOGGER.debug("Patterns: %s", ", ".join(self.patterns))

# Get the state of each zone
# Get the state of all zones
self.states = {}
await self.async_get_zone_data()
_LOGGER.debug("States: %s", self.states)
Expand All @@ -77,18 +75,14 @@ async def async_get_data(self):
async def async_get_zone_data(self, zones: List[str] = None):
"""Retrieves and stores updated state data for one or more zones.
Retrieves data for all zones if zone list is None"""
await self.connect()
try:
_LOGGER.debug("Getting data for zone(s) %s", zones or "[all zones]")
zones = zones or self.zones
controller = (
self._interval_controller
if not zones or len(zones) > 1
else self._entity_controller
zones = list(set(zones or self.zones))
states = await self._hass.async_add_executor_job(
self._controller.getRunPatterns, zones
)
for zone in zones:
state = await self._hass.async_add_executor_job(
controller.getRunPattern, zone
)
for zone, state in states.items():
self.states[zone] = (
state.state,
state.file if state.file != "" else None,
Expand All @@ -98,103 +92,37 @@ async def async_get_zone_data(self, zones: List[str] = None):
_LOGGER.exception(msg)
raise Exception(msg) from ex

async def async_turn_on(self, zones: List[str] = None):
async def async_turn_on(self, zone: str):
"""Turn one or more zones on. Affects all zones if zone list is None"""
await self.connect()
try:
_LOGGER.debug("Turning on zone(s) %s", zones or "[all zones]")
await self._hass.async_add_executor_job(
self._entity_controller.turnOn, zones
)
_LOGGER.debug("Turning on zone %s", zone)
await self._hass.async_add_executor_job(self._controller.turnOn, [zone])
except BaseException as ex: # pylint: disable=broad-except
msg = f"Failed to connect to turn on JellyFish Lighting zone(s) '{zones or '[all zones]'}'"
msg = f"Failed to turn on JellyFish Lighting zone '{zone}'"
_LOGGER.exception(msg)
raise Exception(msg) from ex

async def async_turn_off(self, zones: List[str] = None):
async def async_turn_off(self, zone: str):
"""Turn one or more zones off. Affects all zones if zone list is None"""
await self.connect()
try:
_LOGGER.debug("Turning off zone(s) %s", zones or "[all zones]")
await self._hass.async_add_executor_job(
self._entity_controller.turnOff, zones
)
_LOGGER.debug("Turning off zone %s", zone)
await self._hass.async_add_executor_job(self._controller.turnOff, [zone])
except BaseException as ex: # pylint: disable=broad-except
msg = f"Failed to connect to turn off JellyFish Lighting zone(s) '{zones or '[all zones]'}'"
msg = f"Failed to turn off JellyFish Lighting zone '{zone}'"
_LOGGER.exception(msg)
raise Exception(msg) from ex

async def async_play_pattern(self, pattern: str, zones: List[str] = None):
async def async_play_pattern(self, pattern: str, zone: str):
"""Turn one or more zones on and applies a preset pattern. Affects all zones if zone list is None"""
await self.connect()
try:
_LOGGER.debug(
"Playing pattern '%s' on zone(s) %s",
pattern,
zones or "[all zones]",
)
_LOGGER.debug("Playing pattern '%s' on zone %s", pattern, zone)
await self._hass.async_add_executor_job(
self._entity_controller.playPattern, pattern, zones
self._controller.playPattern, pattern, [zone]
)
except BaseException as ex: # pylint: disable=broad-except
msg = f"Failed to play pattern '{pattern}' on JellyFish Lighting zone(s) '{zones or '[all zones]'}'"
msg = f"Failed to play pattern '{pattern}' on JellyFish Lighting zone '{zone}'"
_LOGGER.exception(msg)
raise Exception(msg) from ex


class JellyFishLightingController(jf.JellyFishController):
"""Wrapper for API to help with reconnections and thread safety"""

def __init__(self, host: str) -> None:
"""Initialize API client."""
self._host = host
self._lock = Lock()
self._reconnect = False
super().__init__(host, True)

def connect(self) -> None:
try:
with self._lock:
# Connect/Reconnect
if self._reconnect and super().connected:
_LOGGER.debug("Disconnecting from JellyFish Lighting controller")
super().disconnect()
if not super().connected:
_LOGGER.debug("Connecting to JellyFish Lighting controller")
super().connect()
except BaseException as ex: # pylint: disable=broad-except
self._reconnect = True
msg = f"Failed to connect/reconnect to JellyFish Lighting controller at {self._host}"
_LOGGER.exception(msg)
raise Exception(msg) from ex

def disconnect(self) -> None:
with self._lock:
super().disconnect()

def getAndStoreZones(self) -> Dict:
self.connect()
with self._lock:
return super().getAndStoreZones()

def getAndStorePatterns(self) -> List[jf.PatternName]:
self.connect()
with self._lock:
return super().getAndStorePatterns()

def getRunPattern(self, zone: str = None) -> jf.RunPatternClass:
self.connect()
with self._lock:
return super().getRunPattern(zone)

def turnOn(self, zones: List[str] = None) -> None:
self.connect()
with self._lock:
super().turnOn(zones)

def turnOff(self, zones: List[str] = None) -> None:
self.connect()
with self._lock:
super().turnOff(zones)

def playPattern(self, pattern: str, zones: List[str] = None) -> None:
self.connect()
with self._lock:
super().playPattern(pattern, zones)
6 changes: 3 additions & 3 deletions custom_components/jellyfish_lighting/light.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,13 +96,13 @@ async def async_turn_on(self, **kwargs): # pylint: disable=unused-argument
self._attr_brightness,
)
if self._attr_effect is not None:
await self.api.async_play_pattern(self._attr_effect, [self.zone])
await self.api.async_play_pattern(self._attr_effect, self.zone)
else:
await self.api.async_turn_on([self.zone])
await self.api.async_turn_on(self.zone)
await self.async_refresh_data()

async def async_turn_off(self, **kwargs): # pylint: disable=unused-argument
"""Turn off the light."""
_LOGGER.debug("In async_turn_off for '%s'. kwargs is %s", self.zone, kwargs)
await self.api.async_turn_off([self.zone])
await self.api.async_turn_off(self.zone)
await self.async_refresh_data()
4 changes: 2 additions & 2 deletions custom_components/jellyfish_lighting/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@
"documentation": "https://github.com/bdunn44/hass-jellyfish-lighting",
"iot_class": "local_polling",
"issue_tracker": "https://github.com/bdunn44/hass-jellyfish-lighting/issues",
"version": "1.0.4",
"version": "1.0.5",
"config_flow": true,
"codeowners": [
"@bdunn44"
],
"requirements": [
"jellyfishlights-py==0.5.0"
"jellyfishlights-py==0.6.0"
]
}
2 changes: 1 addition & 1 deletion requirements_dev.txt
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
homeassistant
jellyfishlights-py==0.5.0
jellyfishlights-py==0.6.0