Skip to content

Commit

Permalink
Implement Push API 💨 (#24)
Browse files Browse the repository at this point in the history
* fixes "title" directly to update a config entry #21

* Fixes 'jellyfish_lighting' calls `async_add_job`, which is deprecated #22

* Set black formatter

* Enable push updates alongside polling

* Fixes #23

* Upgrade jellyfishlights-py library version
  • Loading branch information
bdunn44 authored Jun 3, 2024
1 parent 3a168b9 commit 92097d2
Show file tree
Hide file tree
Showing 10 changed files with 130 additions and 63 deletions.
4 changes: 2 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -18,10 +18,10 @@ jobs:
with:
type: 'zip'
directory: 'custom_components/jellyfish_lighting'
filename: 'jellyfish_lighting.zip'
filename: 'hass-jellyfish-lighting.zip'
- uses: ncipollo/release-action@v1.12.0
with:
artifacts: 'custom_components/jellyfish_lighting/jellyfish_lighting.zip'
artifacts: 'custom_components/jellyfish_lighting/hass-jellyfish-lighting.zip'
draft: true
generateReleaseNotes: true
makeLatest: true
6 changes: 5 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,9 @@
"python.pythonPath": "/usr/local/bin/python",
"files.associations": {
"*.yaml": "home-assistant"
}
},
"[python]": {
"editor.defaultFormatter": "ms-python.black-formatter"
},
"python.formatting.provider": "none"
}
1 change: 1 addition & 0 deletions configuration.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ logger:
default: info
logs:
custom_components.jellyfish_lighting: debug
jellyfishlightspy: debug

# If you need to debug uncomment the lines below (doc: https://www.home-assistant.io/integrations/debugpy/)
debugpy:
Expand Down
14 changes: 11 additions & 3 deletions custom_components/jellyfish_lighting/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""
Custom integration to integrate JellyFish Lighting with Home Assistant.
"""

from homeassistant.config_entries import ConfigEntry
from homeassistant.core import Config, HomeAssistant
from homeassistant.exceptions import ConfigEntryNotReady
Expand Down Expand Up @@ -49,7 +50,14 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
client = JellyfishLightingApiClient(address, entry, hass)
coordinator = JellyfishLightingDataUpdateCoordinator(hass, client=client)
await coordinator.async_refresh()
entry.title = f"{client.name} ({client.hostname})"
# try:
# await client.async_get_data()
# except Exception as e:
# LOGGER.exception("Error fetching %s data", DOMAIN)
# raise ConfigEntryNotReady from e
hass.config_entries.async_update_entry(
entry, title=f"{client.name} ({client.hostname})"
)

if not coordinator.last_update_success:
raise ConfigEntryNotReady
Expand All @@ -65,7 +73,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
)

hass.data[DOMAIN][entry.entry_id] = coordinator
hass.async_add_job(hass.config_entries.async_forward_entry_setup(entry, LIGHT))
hass.async_create_task(hass.config_entries.async_forward_entry_setup(entry, LIGHT))

entry.async_on_unload(entry.add_update_listener(async_reload_entry))
return True
Expand All @@ -77,7 +85,7 @@ class JellyfishLightingDataUpdateCoordinator(DataUpdateCoordinator):
def __init__(self, hass: HomeAssistant, client: JellyfishLightingApiClient) -> None:
"""Initialize."""
self.api = client
self.platforms = []
self.platforms = [LIGHT]
super().__init__(hass, LOGGER, name=DOMAIN, update_interval=SCAN_INTERVAL)

async def _async_update_data(self):
Expand Down
143 changes: 96 additions & 47 deletions custom_components/jellyfish_lighting/api.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,27 @@
"""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
from jellyfishlightspy import JellyFishController, JellyFishException, ZoneState
from .const import LOGGER
from homeassistant.helpers.update_coordinator import (
CoordinatorEntity,
DataUpdateCoordinator,
)
from jellyfishlightspy import (
JellyFishController,
JellyFishException,
ZoneState,
NAME_DATA,
HOSTNAME_DATA,
FIRMWARE_VERSION_DATA,
ZONE_CONFIG_DATA,
PATTERN_LIST_DATA,
PATTERN_CONFIG_DATA,
ZONE_STATE_DATA,
)
from .const import LOGGER, DOMAIN


class JellyfishLightingApiClient:
Expand All @@ -24,6 +41,76 @@ def __init__(
self.name: str = None
self.hostname: str = None
self.version: str = None
self._controller.add_listener(
on_message=self._recieve_push, on_error=self._attempt_reconnect
)

@property
def _coord(self) -> DataUpdateCoordinator:
return self._hass.data[DOMAIN][self._config_entry.entry_id]

@property
def connected(self) -> bool:
"""Indicates whether the client is connected to the controller"""
return self._controller.connected

def register_push_listener(self, entity: CoordinatorEntity) -> None:
"""Adds a listener that is called when push events are received from the controller"""

def listener(*args):
async def update():
entity.schedule_update_ha_state(force_refresh=False)

asyncio.run_coroutine_threadsafe(update(), self._hass.loop)
# self._hass.async_create_task(update())
# self._hass.loop.create_task(entity.async_write_ha_state())
# self._hass.async_create_task(entity.async_write_ha_state)
# asyncio.run_coroutine_threadsafe(
# entity.async_write_ha_state(), self._hass.loop
# )

self._controller.add_listener(
on_open=listener,
on_close=listener,
on_message=listener,
on_error=listener,
)

def _attempt_reconnect(self, *args) -> None:
"""Attempts to reconnect to the controller"""
asyncio.run_coroutine_threadsafe(self.async_connect(), self._hass.loop)

def _recieve_push(self, data):
"""Updates state data when a push event is received from the controller"""

async def update():
if NAME_DATA in data:
self.name = self._controller.name
LOGGER.debug("[PUSH UPDATE] Name: %s", self.name)
elif HOSTNAME_DATA in data:
self.hostname = self._controller.hostname
LOGGER.debug("[PUSH UPDATE] Hostname: %s", self.hostname)
elif FIRMWARE_VERSION_DATA in data:
self.version = self._controller.firmware_version.ver
LOGGER.debug("[PUSH UPDATE] Version: %s", self.version)
elif ZONE_CONFIG_DATA in data:
self.zones = self._controller.zone_names
LOGGER.debug("[PUSH UPDATE] Zones: %s", ", ".join(self.zones))
elif PATTERN_LIST_DATA in data or PATTERN_CONFIG_DATA in data:
patterns = self._controller.pattern_names
patterns.sort()
self.patterns = patterns
LOGGER.debug("[PUSH UPDATE] Patterns: %s", ", ".join(self.patterns))
elif ZONE_STATE_DATA in data:
for zone, state in self._controller.zone_states.items():
if not state:
continue
state = JellyFishLightingZoneData.from_zone_state(state)
self.states[zone] = state
LOGGER.debug("[PUSH UPDATE] '%s' State: %s", zone, state)
self._coord.async_set_updated_data(data)

asyncio.run_coroutine_threadsafe(update(), self._hass.loop)

async def async_connect(self):
"""Establish connection to the controller"""
Expand Down Expand Up @@ -56,42 +143,13 @@ async def async_disconnect(self):
) from ex

async def async_get_data(self):
"""Get data from the API."""
"""Manually fetches data from the controller."""
await self.async_connect()
try:
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.get_pattern_names
)
patterns.sort()
self.patterns = patterns
LOGGER.debug("Patterns: %s", ", ".join(self.patterns))

# Get Zones
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 = zones
LOGGER.debug("Zones: %s", ", ".join(self.zones))

# Get the state of all zones
await self._hass.async_add_executor_job(self._controller.get_pattern_names)
await self._hass.async_add_executor_job(self._controller.get_zone_names)
await self.async_get_zone_states()
except JellyFishException as ex:
raise HomeAssistantError(
Expand All @@ -101,16 +159,11 @@ async def async_get_data(self):
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
)
self.hostname = await self._hass.async_add_executor_job(
self._controller.get_hostname
)
version = await self._hass.async_add_executor_job(
await self._hass.async_add_executor_job(self._controller.get_name)
await self._hass.async_add_executor_job(self._controller.get_hostname)
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}"
Expand All @@ -123,13 +176,9 @@ async def async_get_zone_states(self, zone: str = None):
try:
zones = [zone] if zone else self.zones
LOGGER.debug("Getting data for zone(s) %s", zones or "[all zones]")
states = await self._hass.async_add_executor_job(
await self._hass.async_add_executor_job(
self._controller.get_zone_states, zones
)
for zone, state in states.items():
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}"
Expand Down
1 change: 1 addition & 0 deletions custom_components/jellyfish_lighting/const.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Constants for jellyfish-lighting integration."""

from datetime import timedelta
import logging

Expand Down
5 changes: 5 additions & 0 deletions custom_components/jellyfish_lighting/entity.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""JellyfishLightingEntity class"""

from homeassistant.helpers.update_coordinator import CoordinatorEntity
from homeassistant.helpers.entity import DeviceInfo

Expand All @@ -20,6 +21,10 @@ def __init__(self, coordinator, config_entry):
super().__init__(coordinator)
self.config_entry = config_entry

# @property
# def should_poll(self) -> bool:
# return False

@property
def device_info(self) -> DeviceInfo:
data = self.config_entry.data
Expand Down
15 changes: 7 additions & 8 deletions custom_components/jellyfish_lighting/light.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
"""Switch platform for jellyfish-lighting."""

import re
from typing import Any
from homeassistant.config_entries import ConfigEntry
Expand All @@ -20,14 +21,11 @@
from .entity import JellyfishLightingEntity


async def async_setup_entry(hass, entry, async_add_devices):
async def async_setup_entry(hass, entry, async_add_entities):
"""Setup light platform"""
coordinator = hass.data[DOMAIN][entry.entry_id]
lights = [
JellyfishLightingLight(coordinator, entry, zone)
for zone in coordinator.api.zones
]
async_add_devices(lights)
coord = hass.data[DOMAIN][entry.entry_id]
lights = [JellyfishLightingLight(coord, entry, zone) for zone in coord.api.zones]
async_add_entities(lights)


class JellyfishLightingLight(JellyfishLightingEntity, LightEntity):
Expand All @@ -52,6 +50,7 @@ def __init__(
self._attr_name = zone
self._attr_is_on = False
self._attr_effect = None
self.api.register_push_listener(self)
super().__init__(coordinator, entry)

@property
Expand All @@ -65,7 +64,7 @@ def available(self) -> bool:
self.api.states[self.zone]
except KeyError:
return False
return super().available
return self.api.connected

@property
def effect_list(self) -> list[str]:
Expand Down
2 changes: 1 addition & 1 deletion custom_components/jellyfish_lighting/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
"iot_class": "local_polling",
"issue_tracker": "https://github.com/bdunn44/hass-jellyfish-lighting/issues",
"requirements": [
"jellyfishlights-py==0.7.0"
"jellyfishlights-py==0.8.0"
],
"version": "1.2.0"
}
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
pip>=21.0,<23.1
colorlog
homeassistant
jellyfishlights-py==0.7.0
jellyfishlights-py==0.8.0

0 comments on commit 92097d2

Please sign in to comment.