Skip to content
Permalink

Comparing changes

Choose two branches to see what’s changed or to start a new pull request. If you need to, you can also or learn more about diff comparisons.

Open a pull request

Create a new pull request by comparing changes across two branches. If you need to, you can also . Learn more about diff comparisons here.
base repository: amelchio/aiolifx_effects
Failed to load repositories. Confirm that selected base ref is valid, then try again.
Loading
base: v0.2.1
Choose a base ref
...
head repository: amelchio/aiolifx_effects
Failed to load repositories. Confirm that selected head ref is valid, then try again.
Loading
compare: master
Choose a head ref
  • 15 commits
  • 5 files changed
  • 2 contributors

Commits on Apr 25, 2019

  1. Copy the full SHA
    4df4c58 View commit details
  2. Merge pull request #6 from amelchio/colorloop-no-reset

    Avoid state reset between consecutive colorloops
    amelchio authored Apr 25, 2019
    Copy the full SHA
    077f7f6 View commit details
  3. Release 0.2.2

    amelchio committed Apr 25, 2019
    Copy the full SHA
    cc3c624 View commit details

Commits on Oct 20, 2022

  1. Use get_extended_color_zones on devices that support it

    Signed-off-by: Avi Miller <me@dje.li>
    Djelibeybi committed Oct 20, 2022
    Copy the full SHA
    dbafdde View commit details

Commits on Oct 24, 2022

  1. feat: enable user-configurable saturation range for color_loop effect

    Signed-off-by: Avi Miller <me@dje.li>
    Djelibeybi committed Oct 24, 2022
    Copy the full SHA
    071ba0f View commit details
  2. Copy the full SHA
    6347910 View commit details
  3. Merge pull request #12 from amelchio/pypi-action

    Add a PyPI release action
    amelchio authored Oct 24, 2022
    Copy the full SHA
    249acb6 View commit details
  4. Copy the full SHA
    15d8c66 View commit details
  5. Copy the full SHA
    54dbc14 View commit details
  6. Release 0.3.0 with updated aiolifx depdendency

    Signed-off-by: Avi Miller <me@dje.li>
    Djelibeybi committed Oct 24, 2022
    Copy the full SHA
    6f3b617 View commit details
  7. Disable metadata verification during PyPi publish

    Signed-off-by: Avi Miller <me@dje.li>
    Djelibeybi committed Oct 24, 2022
    Copy the full SHA
    5270cf5 View commit details

Commits on Dec 10, 2022

  1. Fix incorrect detection of LIFX white bulbs

    Signed-off-by: Avi Miller <me@dje.li>
    Djelibeybi committed Dec 10, 2022
    Copy the full SHA
    75cf7c1 View commit details

Commits on Mar 26, 2023

  1. Always create tasks for asyncio.wait()

    Python 3.11 will not wait for coroutines but requires explicit tasks.
    amelchio committed Mar 26, 2023
    Copy the full SHA
    bed87b3 View commit details
  2. Merge pull request #15 from amelchio/await-tasks

    Always create tasks for asyncio.wait()
    amelchio authored Mar 26, 2023
    Copy the full SHA
    a82412a View commit details
  3. Release 0.3.2

    amelchio committed Mar 26, 2023
    Copy the full SHA
    562c5c1 View commit details
Showing with 135 additions and 25 deletions.
  1. +27 −0 .github/workflows/pypi.yml
  2. +59 −22 aiolifx_effects/aiolifx_effects.py
  3. +1 −1 examples/colorloop.py
  4. +46 −0 examples/pulse.py
  5. +2 −2 setup.py
27 changes: 27 additions & 0 deletions .github/workflows/pypi.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
name: Upload Python Package

on:
release:
types: [published]

jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.x'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install build
- name: Build package
run: python -m build
- name: Publish package
uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29
with:
user: __token__
password: ${{ secrets.PYPI_API_TOKEN }}
verify_metadata: false
81 changes: 59 additions & 22 deletions aiolifx_effects/aiolifx_effects.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,30 @@
import asyncio
from functools import partial
import random

from functools import partial
from aiolifx.aiolifx import features_map


# aiolifx waveform modes
WAVEFORM_SINE = 1
WAVEFORM_PULSE = 4

NEUTRAL_WHITE = 3500


def lifx_white(device):
return device.product and device.product in [10, 11, 18]
"""Return true if the device supports neither color nor temperature range."""
max_kelvin = features_map[device.product].get("max_kelvin", None)
min_kelvin = features_map[device.product].get("min_kelvin", None)

if max_kelvin is not None and min_kelvin is not None:
return bool(max_kelvin == min_kelvin)


def has_extended_multizone(device):
"""Return true if the device supports extended multizone messages."""
return features_map[device.product]["extended_multizone"]


class PreState:
"""Structure describing a power/color state."""
@@ -77,48 +91,61 @@ async def start(self, effect, participants):
effect.conductor = self

# Restore previous state
await self._stop_nolock(participants)
await self._stop_nolock(participants, effect)

# Remember the current state
tasks = []
for device in participants:
tasks.append(AwaitAioLIFX().wait(device.get_color))
if device.color_zones:
for zone in range(0, len(device.color_zones), 8):
tasks.append(AwaitAioLIFX().wait(partial(device.get_color_zones, start_index=zone)))
await asyncio.wait(tasks)
if device.version is None:
response = await AwaitAioLIFX().wait(device.get_version)
device.product = response.product
if not self.running.get(device.mac_addr):
tasks.append(asyncio.create_task(AwaitAioLIFX().wait(device.get_color)))
if device.color_zones:
if has_extended_multizone(device):
tasks.append(asyncio.create_task(AwaitAioLIFX().wait(device.get_extended_color_zones)))
else:
for zone in range(0, len(device.color_zones), 8):
tasks.append(asyncio.create_task(AwaitAioLIFX().wait(partial(device.get_color_zones, start_index=zone))))
if tasks:
await asyncio.wait(tasks)

for device in participants:
pre_state = PreState(device)
running = self.running.get(device.mac_addr)
pre_state = running.pre_state if running else PreState(device)
self.running[device.mac_addr] = RunningEffect(effect, pre_state)

# Powered off zones report zero brightness. Get the real values.
await self._fixup_multizone(participants)
# Powered off zones report zero brightness on older multizone devices. Get the real values.
if has_extended_multizone(device) is False:
await self._fixup_multizone(participants)

self.loop.create_task(effect.async_perform(participants))

async def stop(self, devices):
async with self.lock:
await self._stop_nolock(devices)

async def _stop_nolock(self, devices):
async def _stop_nolock(self, devices, new_effect=None):
tasks = []
for device in devices:
tasks.append(self.loop.create_task(self._stop_one(device)))
tasks.append(self.loop.create_task(self._stop_one(device, new_effect)))
if tasks:
await asyncio.wait(tasks)

async def _stop_one(self, device):
running = self.running.get(device.mac_addr, None)
async def _stop_one(self, device, new_effect):
running = self.running.get(device.mac_addr)
if not running:
return
effect = running.effect

del self.running[device.mac_addr]

index = next(i for i,p in enumerate(effect.participants) if p.mac_addr == device.mac_addr)
effect.participants.pop(index)

if new_effect and effect.inherit_prestate(new_effect):
return

del self.running[device.mac_addr]

if not running.pre_state.power:
device.set_power(False)
await asyncio.sleep(0.3)
@@ -150,7 +177,7 @@ async def _fixup_multizone(self, participants):
async def powertoggle(state):
tasks = []
for device in fixup:
tasks.append(AwaitAioLIFX().wait(partial(device.set_power, state)))
tasks.append(asyncio.create_task(AwaitAioLIFX().wait(partial(device.set_power, state))))
await asyncio.wait(tasks)
await asyncio.sleep(0.3)

@@ -161,7 +188,7 @@ async def powertoggle(state):
tasks = []
for device in fixup:
for zone in range(0, len(device.color_zones), 8):
tasks.append(AwaitAioLIFX().wait(partial(device.get_color_zones, start_index=zone)))
tasks.append(asyncio.create_task(AwaitAioLIFX().wait(partial(device.get_color_zones, start_index=zone))))
await asyncio.wait(tasks)

# Update pre_state colors
@@ -213,6 +240,10 @@ async def from_poweroff_hsbk(self, device):
def running(self, device):
return self.conductor.running[device.mac_addr]

def inherit_prestate(self, other):
"""Returns True if two effects can run without a reset."""
return False


class EffectPulse(LIFXEffect):
"""Representation of a pulse effect."""
@@ -311,7 +342,7 @@ async def effect_color(self, device):
class EffectColorloop(LIFXEffect):
"""Representation of a colorloop effect."""

def __init__(self, power_on=True, period=None, change=None, spread=None, brightness=None, transition=None):
def __init__(self, power_on=True, period=None, change=None, spread=None, brightness=None, saturation_min=None, saturation_max=None, transition=None):
"""Initialize the colorloop effect."""
super().__init__(power_on)
self.name = 'colorloop'
@@ -320,8 +351,13 @@ def __init__(self, power_on=True, period=None, change=None, spread=None, brightn
self.change = change if change else 20
self.spread = spread if spread else 30
self.brightness = brightness
self.saturation = [saturation_min or 52428, saturation_max or 65535]
self.transition = transition

def inherit_prestate(self, other):
"""Returns True if two effects can run without a reset."""
return type(self) == type(other)

async def async_play(self, **kwargs):
"""Play the effect on all lights."""
# Random start
@@ -331,7 +367,6 @@ async def async_play(self, **kwargs):
while self.participants:
hue = (hue + direction*self.change) % 360
lhue = hue

random.shuffle(self.participants)

for device in self.participants:
@@ -345,9 +380,11 @@ async def async_play(self, **kwargs):
else:
brightness = self.running(device).pre_state.color[2]

saturation = int(random.uniform(self.saturation[0], self.saturation[1]))

hsbk = [
int(65535/360*lhue),
int(random.uniform(0.8, 1.0)*65535),
saturation,
brightness,
NEUTRAL_WHITE,
]
2 changes: 1 addition & 1 deletion examples/colorloop.py
Original file line number Diff line number Diff line change
@@ -16,7 +16,7 @@ def register(self, bulb):
def unregister(self, bulb):
del self.bulbs[bulb.mac_addr]

loop = asyncio.get_event_loop()
loop = asyncio.get_event_loop_policy().get_event_loop()

conductor = aiolifx_effects.Conductor(loop=loop)

46 changes: 46 additions & 0 deletions examples/pulse.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
#!/usr/bin/env python3

import asyncio
from functools import partial

import aiolifx
import aiolifx_effects

class Bulbs:
def __init__(self):
self.bulbs = {}

def register(self, bulb):
self.bulbs[bulb.mac_addr] = bulb

def unregister(self, bulb):
del self.bulbs[bulb.mac_addr]

loop = asyncio.get_event_loop_policy().get_event_loop()

conductor = aiolifx_effects.Conductor(loop=loop)

mybulbs = Bulbs()

discover = loop.create_datagram_endpoint(
partial(aiolifx.LifxDiscovery, loop, mybulbs),
local_addr=('0.0.0.0', 56700))

loop.create_task(discover)

# Probe
sleep = loop.create_task(asyncio.sleep(3))
loop.run_until_complete(sleep)

# Run effect
devices = list(mybulbs.bulbs.values())
effect = aiolifx_effects.EffectPulse(power_on=True, mode="blink", cycles=3)
loop.create_task(conductor.start(effect, devices))

# Stop effect in a while
stop = conductor.stop(devices)
loop.call_later(3, lambda: loop.create_task(stop))

# Wait for completion
sleep = loop.create_task(asyncio.sleep(4))
loop.run_until_complete(sleep)
4 changes: 2 additions & 2 deletions setup.py
Original file line number Diff line number Diff line change
@@ -5,8 +5,8 @@
setup(
name='aiolifx_effects',
packages=['aiolifx_effects'],
version='0.2.1',
install_requires=['aiolifx>=0.5.0'],
version='0.3.2',
install_requires=['aiolifx>=0.8.6'],
description='aiolifx light effects',
author='Anders Melchiorsen',
author_email='amelchio@nogoto.net',