Skip to content

Commit

Permalink
Improve API and Metadata (#16)
Browse files Browse the repository at this point in the history
* Adapt to API library refactoring

* Allow multiple controllers!

* Use library release version 0.7.0
  • Loading branch information
bdunn44 authored May 24, 2023
1 parent f60f0b5 commit 3a168b9
Show file tree
Hide file tree
Showing 8 changed files with 196 additions and 141 deletions.
22 changes: 11 additions & 11 deletions custom_components/jellyfish_lighting/__init__.py
Original file line number Diff line number Diff line change
@@ -1,20 +1,18 @@
"""
Custom integration to integrate JellyFish Lighting with Home Assistant.
"""
from datetime import timedelta
import logging
from homeassistant.config_entries import ConfigEntry
from homeassistant.core import Config, HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
from homeassistant.helpers import device_registry as dr
from homeassistant.helpers import device_registry

from .api import JellyfishLightingApiClient

from .const import (
LOGGER,
SCAN_INTERVAL,
CONF_HOST,
CONF_ADDRESS,
DOMAIN,
NAME,
DEVICE,
Expand Down Expand Up @@ -43,26 +41,27 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
"data": entry.data,
},
)
entry.title = DEVICE
if hass.data.get(DOMAIN) is None:
hass.data.setdefault(DOMAIN, {})
LOGGER.info(STARTUP_MESSAGE)

host = entry.data.get(CONF_HOST)
client = JellyfishLightingApiClient(host, entry, hass)
address = entry.data.get(CONF_ADDRESS)
client = JellyfishLightingApiClient(address, entry, hass)
coordinator = JellyfishLightingDataUpdateCoordinator(hass, client=client)
await coordinator.async_refresh()
entry.title = f"{client.name} ({client.hostname})"

if not coordinator.last_update_success:
raise ConfigEntryNotReady

device_registry = dr.async_get(hass)
device_registry.async_get_or_create(
registry = device_registry.async_get(hass)
registry.async_get_or_create(
config_entry_id=entry.entry_id,
identifiers={(DOMAIN, host)},
identifiers={(DOMAIN, client.hostname)},
name=client.name,
manufacturer=NAME,
model=DEVICE,
name=NAME,
sw_version=client.version,
)

hass.data[DOMAIN][entry.entry_id] = coordinator
Expand All @@ -86,6 +85,7 @@ async def _async_update_data(self):
try:
return await self.api.async_get_data()
except Exception as exception:
LOGGER.exception("Error fetching %s data", DOMAIN)
raise UpdateFailed() from exception


Expand Down
207 changes: 121 additions & 86 deletions custom_components/jellyfish_lighting/api.py
Original file line number Diff line number Diff line change
@@ -1,186 +1,221 @@
"""Sample API Client."""
import asyncio
from typing import List, Tuple, Dict
from homeassistant.core import HomeAssistant
from homeassistant.exceptions import HomeAssistantError
from homeassistant.config_entries import ConfigEntry
import jellyfishlightspy as jf
from jellyfishlightspy import JellyFishController, JellyFishException, ZoneState
from .const import LOGGER


class JellyfishLightingApiClient:
"""API Client for JellyFish Lighting"""

def __init__(
self, host: str, config_entry: ConfigEntry, hass: HomeAssistant
self, address: str, config_entry: ConfigEntry, hass: HomeAssistant
) -> None:
"""Initialize API client."""
self.host = host
self.address = address
self._config_entry = config_entry
self._hass = hass
self._controller = jf.JellyFishController(host, False)
self._controller = JellyFishController(address)
self.zones: List[str] = []
self.states: Dict[str, JellyFishLightingZoneData] = {}
self.patterns: List[str] = []
self.name: str = None
self.hostname: str = None
self.version: str = None

async def async_connect(self):
"""Establish connection to the controller"""
if self._controller.connected:
return
try:
if not self._controller.connected:
LOGGER.debug(
"Connecting to the JellyFish Lighting controller at %s", self.host
)
await asyncio.wait_for(
self._hass.async_add_executor_job(self._controller.connect),
timeout=5,
)
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 # pylint: disable=broad-exception-raised
LOGGER.debug(
"Connecting to the JellyFish Lighting controller at %s",
self.address,
)
await self._hass.async_add_executor_job(self._controller.connect, 5)
except JellyFishException as ex:
raise HomeAssistantError(
f"Failed to connect to JellyFish Lighting controller at {self.address}"
) from ex

async def async_disconnect(self):
"""Disconnects from the controller"""
if not self._controller.connected:
return
try:
LOGGER.debug(
"Disconnecting from the JellyFish Lighting controller at %s",
self.address,
)
await self._hass.async_add_executor_job(self._controller.disconnect, 5)
except JellyFishException as ex:
raise HomeAssistantError(
f"Failed to disconnect from JellyFish Lighting controller at {self.address}"
) from ex

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

# Get controller configuration
await self.async_get_controller_info()
LOGGER.debug(
"Hostname: %s, Name: %s, Version: %s",
self.hostname,
self.name,
self.version,
)

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

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

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

self.zones = list(zones)
self.zones = zones
LOGGER.debug("Zones: %s", ", ".join(self.zones))

# Get the state of all zones
await self.async_get_zone_data()
except BaseException as ex: # pylint: disable=broad-except
msg = (
f"Failed to get data from JellyFish Lighting controller at {self.host}"
await self.async_get_zone_states()
except JellyFishException as ex:
raise HomeAssistantError(
f"Failed to get data from JellyFish Lighting controller at {self.address}"
) from ex

async def async_get_controller_info(self):
"""Retrieves basic information from the controller"""
try:
self.name = await self._hass.async_add_executor_job(
self._controller.get_name
)
LOGGER.exception(msg)
raise Exception(msg) from ex # pylint: disable=broad-exception-raised
self.hostname = await self._hass.async_add_executor_job(
self._controller.get_hostname
)
version = await self._hass.async_add_executor_job(
self._controller.get_firmware_version
)
self.version = version.ver
except JellyFishException as ex:
raise HomeAssistantError(
f"Failed to retrieve JellyFish controller information from {self.address}"
) from ex

async def async_get_zone_data(self, zones: List[str] = None):
async def async_get_zone_states(self, zone: 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.async_connect()
try:
zones = [zone] if zone else self.zones
LOGGER.debug("Getting data for zone(s) %s", zones or "[all zones]")
zones = list(set(zones or self.zones))
states = await self._hass.async_add_executor_job(
self._controller.getRunPatterns, zones
self._controller.get_zone_states, zones
)

for zone, state in states.items():
if zone not in self.states:
self.states[zone] = JellyFishLightingZoneData()
data = self.states[zone]
if (
state.file == ""
and state.data
and state.data.numOfLeds == "Color"
and len(state.data.colors) == 3
):
# state is solid RGB
data.state = state.state
data.file = None
data.color = tuple(state.data.colors)
data.brightness = state.data.colorPos.brightness
else:
data.state = state.state
data.file = state.file if state.file != "" else None
data.color = None
data.brightness = None

LOGGER.debug("%s: (%s)", zone, data)
except BaseException as ex: # pylint: disable=broad-except
msg = f"Failed to get zone data for [{', '.join(zones)}] from JellyFish Lighting controller at {self.host}"
LOGGER.exception(msg)
raise Exception(msg) from ex # pylint: disable=broad-exception-raised
data = JellyFishLightingZoneData.from_zone_state(state)
self.states[zone] = data
LOGGER.debug("%s: %s", zone, data)
except JellyFishException as ex:
raise HomeAssistantError(
f"Failed to get zone data for [{', '.join(zones)}] from JellyFish Lighting controller at {self.address}"
) from ex

async def async_turn_on(self, zone: str):
"""Turn one or more zones on. Affects all zones if zone list is None"""
await self.async_connect()
try:
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 turn on JellyFish Lighting zone '{zone}'"
LOGGER.exception(msg)
raise Exception(msg) from ex # pylint: disable=broad-exception-raised
await self._hass.async_add_executor_job(self._controller.turn_on, [zone])
except JellyFishException as ex:
raise HomeAssistantError(
f"Failed to turn on JellyFish Lighting zone '{zone}'"
) from ex

async def async_turn_off(self, zone: str):
"""Turn one or more zones off. Affects all zones if zone list is None"""
await self.async_connect()
try:
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 turn off JellyFish Lighting zone '{zone}'"
LOGGER.exception(msg)
raise Exception(msg) from ex # pylint: disable=broad-exception-raised
await self._hass.async_add_executor_job(self._controller.turn_off, [zone])
except JellyFishException as ex:
raise HomeAssistantError(
f"Failed to turn off JellyFish Lighting zone '{zone}'"
) from ex

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

async def async_send_color(
async def async_apply_color(
self, rgb: Tuple[int, int, int], brightness: int, zone: str
):
"""Turn one or more zones on and set all lights to a single color at the given brightness.
Affects all zones if zone list is None"""
await self.async_connect()
try:
LOGGER.debug(
"Playing color %s at %s brightness to zone(s) %s",
"Applying color %s at %s brightness to zone %s",
rgb,
brightness,
zone,
)
await self._hass.async_add_executor_job(
self._controller.sendColor, rgb, brightness, zone
self._controller.apply_color, rgb, brightness, [zone]
)
except BaseException as ex: # pylint: disable=broad-except
msg = f"Failed to play color '{rgb}' at {brightness}% brightness on JellyFish Lighting zone '{zone}'"
LOGGER.exception(msg)
raise Exception(msg) from ex # pylint: disable=broad-exception-raised
except JellyFishException as ex:
raise HomeAssistantError(
f"Failed to apply color '{rgb}' at {brightness}% brightness on JellyFish Lighting zone '{zone}'"
) from ex


class JellyFishLightingZoneData:
"""Simple class to store the state of a zone"""

def __init__(
self,
state: bool = None,
is_on: bool = None,
file: str = None,
color: tuple[int, int, int] = None,
brightness: int = None,
):
self.state = state
self.is_on = is_on
self.file = file
self.color = color
self.brightness = brightness

def __str__(self) -> str:
return f"state: {self.state}, file: {self.file}, color: {self.color}, brightness: {self.brightness}"
@classmethod
def from_zone_state(cls, state: ZoneState):
"""Instantiates the class from the data returned by the API"""
data = cls(state.is_on, state.file or None)
if state.data:
data.brightness = state.data.runData.brightness
if state.data.type == "Color" and len(state.data.colors) == 3:
data.color = tuple(state.data.colors)
return data

def __repr__(self) -> str:
return str(vars(self))
Loading

0 comments on commit 3a168b9

Please sign in to comment.