Add extended color light

This commit is contained in:
Scott Shawcroft 2024-10-21 16:37:25 -07:00
parent 572c7a7fb7
commit 0a8b706903
No known key found for this signature in database
15 changed files with 504 additions and 21 deletions

View file

@ -3,6 +3,7 @@ import enum
from circuitmatter.data_model import (
Cluster,
Enum8,
Map16,
NumberAttribute,
OctetStringAttribute,
BoolAttribute,
@ -13,6 +14,14 @@ from circuitmatter.data_model import (
from circuitmatter import tlv
class ThreadCapabilitiesBitmap(Map16):
IS_BORDER_ROUTER_CAPABLE = 1 << 0
IS_ROUTER_CAPABLE = 1 << 1
IS_SLEEPY_END_DEVICE_CAPABLE = 1 << 2
IS_FULL_THREAD_DEVICE = 1 << 3
IS_SYNCHRONIZED_SLEEPY_END_DEVICE_CAPABLE = 1 << 4
class NetworkCommissioningCluster(Cluster):
CLUSTER_ID = 0x0031
@ -101,7 +110,10 @@ class NetworkCommissioningCluster(Cluster):
8, WifiBandEnum, feature=FeatureBitmap.WIFI_NETWORK_INTERFACE, F_fixed=True
)
supported_thread_features = BitmapAttribute(
9, feature=FeatureBitmap.THREAD_NETWORK_INTERFACE, F_fixed=True
9,
ThreadCapabilitiesBitmap,
feature=FeatureBitmap.THREAD_NETWORK_INTERFACE,
F_fixed=True,
)
thread_version = NumberAttribute(
10,

View file

@ -0,0 +1,92 @@
import enum
from circuitmatter import data_model
from circuitmatter import tlv
class FeatureBitmap(enum.IntFlag):
ON_OFF = 1 << 0
LIGHTING = 1 << 1
FREQUENCY = 1 << 2
class OptionsBitmap(data_model.Map8):
ExecuteIfOff = 0
CoupleColorTempToLevel = 1
class MoveModeEnum(data_model.Enum8):
UP = 0
DOWN = 1
class StepModeEnum(data_model.Enum8):
UP = 0
DOWN = 1
class LevelControl(data_model.Cluster):
CLUSTER_ID = 0x0008
CurrentLevel = data_model.NumberAttribute(
0x0000, signed=False, bits=8, N_nonvolatile=True, X_nullable=True
)
RemainingTime = data_model.NumberAttribute(
0x0001, signed=False, bits=16, default=0, feature=FeatureBitmap.LIGHTING
)
MinLevel = data_model.NumberAttribute(
0x0002,
signed=False,
bits=8,
default=lambda features: 1 if features & FeatureBitmap.LIGHTING else 0,
)
MaxLevel = data_model.NumberAttribute(0x0003, signed=False, bits=8, default=254)
OnLevel = data_model.NumberAttribute(0x0011, signed=False, bits=8, X_nullable=True)
Options = data_model.BitmapAttribute(0x000F, OptionsBitmap, default=0)
StartUpCurrentLevel = data_model.NumberAttribute(
0x4000,
signed=False,
bits=8,
X_nullable=True,
N_nonvolatile=True,
feature=FeatureBitmap.LIGHTING,
)
class MoveToLevel(tlv.Structure):
Level = tlv.IntMember(0, signed=False, octets=1)
TransitionTime = tlv.IntMember(1, signed=False, octets=2, nullable=True)
OptionsMask = tlv.BitmapMember(2, OptionsBitmap, default=0)
OptionsOverride = tlv.BitmapMember(3, OptionsBitmap, default=0)
move_to_level = data_model.Command(0x00, MoveToLevel)
class Move(tlv.Structure):
MoveMode = tlv.EnumMember(0, MoveModeEnum)
Rate = tlv.IntMember(1, signed=True, octets=1, nullable=True)
OptionsMask = tlv.BitmapMember(2, OptionsBitmap, default=0)
OptionsOverride = tlv.BitmapMember(3, OptionsBitmap, default=0)
move = data_model.Command(0x01, Move)
class Step(tlv.Structure):
StepMode = tlv.EnumMember(0, StepModeEnum)
StepSize = tlv.IntMember(1, signed=True, octets=1)
TransitionTime = tlv.IntMember(2, signed=False, octets=2, nullable=True)
OptionsMask = tlv.BitmapMember(3, OptionsBitmap, default=0)
OptionsOverride = tlv.BitmapMember(4, OptionsBitmap, default=0)
step = data_model.Command(0x02, Step)
class Stop(tlv.Structure):
OptionsMask = tlv.BitmapMember(0, OptionsBitmap, default=0)
OptionsOverride = tlv.BitmapMember(1, OptionsBitmap, default=0)
stop = data_model.Command(0x03, Stop)
move_to_level_with_on_off = data_model.Command(0x04, MoveToLevel)
move_with_on_off = data_model.Command(0x05, Move)
step_with_on_off = data_model.Command(0x06, Step)
stop_with_on_off = data_model.Command(0x07, Stop)

View file

@ -1,7 +1,15 @@
import enum
from circuitmatter import data_model
from circuitmatter import tlv
class FeatureBitmap(enum.IntFlag):
LIGHTING = 1 << 0
DEAD_FRONT_BEHAVIOR = 1 << 1
OFF_ONLY = 1 << 2
class StartUpOnOffEnum(data_model.Enum8):
OFF = 0
ON = 1
@ -12,11 +20,21 @@ class OnOff(data_model.Cluster):
CLUSTER_ID = 0x0006
OnOff = data_model.BoolAttribute(0x0000, default=False, N_nonvolatile=True)
GlobalSceneControl = data_model.BoolAttribute(0x4000, default=True)
OnTime = data_model.NumberAttribute(0x4001, signed=False, bits=16, default=0)
OffWaitTime = data_model.NumberAttribute(0x4002, signed=False, bits=16, default=0)
GlobalSceneControl = data_model.BoolAttribute(
0x4000, default=True, feature=FeatureBitmap.LIGHTING
)
OnTime = data_model.NumberAttribute(
0x4001, signed=False, bits=16, default=0, feature=FeatureBitmap.LIGHTING
)
OffWaitTime = data_model.NumberAttribute(
0x4002, signed=False, bits=16, default=0, feature=FeatureBitmap.LIGHTING
)
StartUpOnOff = data_model.EnumAttribute(
0x4003, StartUpOnOffEnum, N_nonvolatile=True, X_nullable=True
0x4003,
StartUpOnOffEnum,
N_nonvolatile=True,
X_nullable=True,
feature=FeatureBitmap.LIGHTING,
)
off = data_model.Command(0x00, None)

View file

@ -0,0 +1,122 @@
import enum
from circuitmatter import data_model, tlv
class FeatureBitmap(enum.IntFlag):
HUE_SATURATION = 1 << 0
ENHANCED_HUE = 1 << 1
COLOR_LOOP = 1 << 2
XY = 1 << 3
COLOR_TEMPERATURE = 1 << 4
class OptionsBitmap(data_model.Map8):
EXECUTE_IF_OFF = 1 << 0
class Direction(data_model.Enum8):
SHORTEST_DISTANCE = 0
LONGEST_DISTANCE = 1
UP = 2
DOWN = 3
class MoveMode(data_model.Enum8):
STOP = 0
UP = 1
DOWN = 3
class StepMode(data_model.Enum8):
UP = 1
DOWN = 3
class ColorMode(data_model.Enum8):
HUE_SATURATION = 0
XY = 1
COLOR_TEMPERATURE = 2
class ColorControl(data_model.Cluster):
CLUSTER_ID = 0x0300
REVISION = 6
CurrentHue = data_model.NumberAttribute(0x0000, signed=False, bits=8, default=0)
CurrentSaturation = data_model.NumberAttribute(
0x0001, signed=False, bits=8, default=0
)
RemainingTime = data_model.NumberAttribute(0x0002, signed=False, bits=16, default=0)
CurrentX = data_model.NumberAttribute(0x0003, signed=False, bits=16, default=0)
CurrentY = data_model.NumberAttribute(0x0004, signed=False, bits=16, default=0)
DriftCompensation = data_model.NumberAttribute(
0x0005, signed=False, bits=8, default=0
)
CompensationText = data_model.UTF8StringAttribute(0x0006, default="")
ColorTemperature = data_model.NumberAttribute(
0x0007, signed=False, bits=16, default=0
)
ColorMode = data_model.EnumAttribute(0x0008, data_model.Enum8, default=0)
Options = data_model.BitmapAttribute(0x000F, OptionsBitmap, default=0)
ColorCapabilities = data_model.BitmapAttribute(0x400A, FeatureBitmap, default=0)
class MoveToHue(tlv.Structure):
Hue = tlv.IntMember(0, signed=False, octets=1, maximum=254)
Direction = tlv.EnumMember(1, Direction)
TransitionTime = tlv.IntMember(2, signed=False, octets=2, nullable=True)
OptionsMask = tlv.BitmapMember(3, OptionsBitmap, default=0)
OptionsOverride = tlv.BitmapMember(4, OptionsBitmap, default=0)
move_to_hue = data_model.Command(0x00, MoveToHue)
class MoveHue(tlv.Structure):
MoveMode = tlv.EnumMember(0, Direction)
Rate = tlv.IntMember(1, signed=True, octets=1, nullable=True)
OptionsMask = tlv.BitmapMember(2, OptionsBitmap, default=0)
OptionsOverride = tlv.BitmapMember(3, OptionsBitmap, default=0)
move_hue = data_model.Command(0x01, MoveHue)
class StepHue(tlv.Structure):
StepMode = tlv.EnumMember(0, StepMode)
StepSize = tlv.IntMember(1, signed=True, octets=1)
TransitionTime = tlv.IntMember(2, signed=False, octets=1)
OptionsMask = tlv.BitmapMember(3, OptionsBitmap, default=0)
OptionsOverride = tlv.BitmapMember(4, OptionsBitmap, default=0)
step_hue = data_model.Command(0x02, StepHue)
class MoveToSaturation(tlv.Structure):
Saturation = tlv.IntMember(0, signed=False, octets=1)
TransitionTime = tlv.IntMember(1, signed=False, octets=2)
OptionsMask = tlv.BitmapMember(2, OptionsBitmap, default=0)
OptionsOverride = tlv.BitmapMember(3, OptionsBitmap, default=0)
move_to_saturation = data_model.Command(0x03, MoveToSaturation)
class MoveSaturation(tlv.Structure):
MoveMode = tlv.EnumMember(0, MoveMode)
Rate = tlv.IntMember(1, signed=True, octets=1, nullable=True)
OptionsMask = tlv.BitmapMember(2, OptionsBitmap, default=0)
OptionsOverride = tlv.BitmapMember(3, OptionsBitmap, default=0)
move_saturation = data_model.Command(0x04, MoveSaturation)
class StepSaturation(tlv.Structure):
StepMode = tlv.EnumMember(0, StepMode)
StepSize = tlv.IntMember(1, signed=True, octets=1)
TransitionTime = tlv.IntMember(2, signed=False, octets=2, nullable=True)
OptionsMask = tlv.BitmapMember(3, OptionsBitmap, default=0)
OptionsOverride = tlv.BitmapMember(4, OptionsBitmap, default=0)
step_saturation = data_model.Command(0x05, StepSaturation)
class MoveToHueAndSaturation(tlv.Structure):
Hue = tlv.IntMember(0, signed=False, octets=1)
Saturation = tlv.IntMember(1, signed=False, octets=1)
TransitionTime = tlv.IntMember(2, signed=False, octets=2, nullable=True)
OptionsMask = tlv.BitmapMember(3, OptionsBitmap, default=0)
OptionsOverride = tlv.BitmapMember(4, OptionsBitmap, default=0)
move_to_hue_and_saturation = data_model.Command(0x06, MoveToHueAndSaturation)

View file

@ -21,6 +21,14 @@ class Enum16(enum.IntEnum):
pass
class Map8(enum.IntFlag):
pass
class Map16(enum.IntFlag):
pass
class Uint16(tlv.IntMember):
def __init__(self, _id=None, minimum=0, **kwargs):
super().__init__(_id, signed=False, octets=2, minimum=minimum, **kwargs)
@ -88,6 +96,8 @@ class Attribute:
def __get__(self, instance, cls):
v = instance._attribute_values.get(self.id, None)
if v is None:
if callable(self.default):
return self.default(instance.feature_map)
return self.default
return v
@ -270,8 +280,11 @@ class UTF8StringAttribute(Attribute):
return self.member.encode(value)
class BitmapAttribute(Attribute):
pass
class BitmapAttribute(NumberAttribute):
def __init__(self, _id, enum_type, **kwargs):
self.enum_type = enum_type
bits = 8 if issubclass(enum_type, Map8) else 16
super().__init__(_id, signed=False, bits=bits, **kwargs)
class Command:

View file

@ -1,5 +1,24 @@
from .dimmable import DimmableLight
from circuitmatter.clusters.lighting import color_control
class ColorTemperatureLight(DimmableLight):
DEVICE_TYPE_ID = 0x010C
REVISION = 4
def __init__(self, name):
super().__init__(name)
self._color_control = color_control.ColorControl()
self._color_control.feature_map |= color_control.FeatureBitmap.COLOR_TEMPERATURE
self.servers.append(self._color_control)
@property
def color_rgb(self):
raise NotImplementedError()
@color_rgb.setter
def color_rgb(self, value):
raise NotImplementedError()

View file

@ -1,5 +1,35 @@
from .on_off import OnOffLight
from circuitmatter.clusters.general import level_control
class DimmableLight(OnOffLight):
DEVICE_TYPE_ID = 0x0101
REVISION = 3
def __init__(self, name):
super().__init__(name)
self._level_control = level_control.LevelControl()
self._level_control.feature_map |= level_control.FeatureBitmap.LIGHTING
self._level_control.min_level = 1
self._level_control.max_level = 254
self.servers.append(self._level_control)
self._level_control.move_to_level_with_on_off = self._move_to_level_with_on_off
def _move_to_level_with_on_off(self, session, value):
try:
self.brightness = value.Level / self._level_control.max_level
except Exception as e:
print(f"Error setting brightness: {e}")
return
self._level_control.CurrentLevel = value.Level
@property
def brightness(self):
return self._level_control.CurrentLevel / self._level_control.max_level
@brightness.setter
def brightness(self, value):
raise NotImplementedError()

View file

@ -1,5 +1,37 @@
import colorsys
from .color_temperature import ColorTemperatureLight
from circuitmatter.clusters.lighting import color_control
class ExtendedColorLight(ColorTemperatureLight):
DEVICE_TYPE_ID = 0x010D
REVISION = 4
def __init__(self, name):
super().__init__(name)
self._color_control.feature_map |= (
color_control.FeatureBitmap.HUE_SATURATION
| color_control.FeatureBitmap.ENHANCED_HUE
| color_control.FeatureBitmap.COLOR_LOOP
| color_control.FeatureBitmap.XY
)
self._color_control.move_to_hue_and_saturation = (
self._move_to_hue_and_saturation
)
def _move_to_hue_and_saturation(self, session, value):
try:
r, g, b = colorsys.hsv_to_rgb(value.Hue / 254, value.Saturation / 254, 1)
self.color_rgb = int(r * 255) << 16 | int(g * 255) << 8 | int(b * 255)
except Exception as e:
print(f"Error setting color: {e}")
return
self._color_control.ColorMode = color_control.ColorMode.HUE_SATURATION
self._color_control.CurrentHue = value.Hue
self._color_control.CurrentSaturation = value.Saturation

View file

@ -1,5 +1,5 @@
from circuitmatter.clusters.general.identify import Identify
from circuitmatter.clusters.general.on_off import OnOff
from circuitmatter.clusters.general import on_off
from .. import simple_device
@ -14,15 +14,30 @@ class OnOffLight(simple_device.SimpleDevice):
self._identify = Identify()
self.servers.append(self._identify)
self._on_off = OnOff()
self._on_off.on = self.on
self._on_off.off = self.off
self._on_off = on_off.OnOff()
self._on_off.on = self._on
self._on_off.off = self._off
self._on_off.feature_map |= on_off.FeatureBitmap.LIGHTING
self.servers.append(self._on_off)
def on(self, session):
print("on!")
def _on(self, session):
try:
self.on()
except Exception as e:
print(f"Error turning on light: {e}")
return
self._on_off.on_off = True
def off(self, session):
print("off!")
def _off(self, session):
try:
self.off()
except Exception as e:
print(f"Error turning off light: {e}")
return
self._on_off.on_off = False
def on(self):
raise NotImplementedError()
def off(self):
raise NotImplementedError()

View file

@ -6,10 +6,15 @@ class SimpleDevice:
self.name = name
self.servers = []
self.descriptor = descriptor.DescriptorCluster()
device_type = descriptor.DescriptorCluster.DeviceTypeStruct()
device_type.DeviceType = self.DEVICE_TYPE_ID
device_type.Revision = self.REVISION
self.descriptor.DeviceTypeList = [device_type]
device_types = []
for superclass in type(self).__mro__:
if not hasattr(superclass, "DEVICE_TYPE_ID"):
continue
device_type = descriptor.DescriptorCluster.DeviceTypeStruct()
device_type.DeviceType = superclass.DEVICE_TYPE_ID
device_type.Revision = superclass.REVISION
device_types.append(device_type)
self.descriptor.DeviceTypeList = device_types
self.descriptor.PartsList = []
self.descriptor.ServerList = []
self.descriptor.ClientList = []

View file

@ -579,6 +579,11 @@ class EnumMember(IntMember):
return self.enum_class(value).name
class BitmapMember(EnumMember):
def print(self, value):
return repr(self.enum_class(value))
class FloatMember(NumberMember[float, _OPT, _NULLABLE]):
def __init__(
self,

View file

@ -13,10 +13,10 @@ class LED(on_off.OnOffLight):
self._led = led
self._led.direction = digitalio.Direction.OUTPUT
def on(self, session):
def on(self):
self._led.value = True
def off(self, session):
def off(self):
self._led.value = False

View file

@ -0,0 +1,91 @@
"""Pure Python implementation of the Matter IOT protocol."""
import json
import pathlib
import socket
import time
import circuitmatter as cm
from circuitmatter.device_types.lighting import extended_color
from circuitmatter.utility import random
from circuitmatter.utility.recording import RecordingSocketPool, RecordingRandom
from circuitmatter.utility.replay import ReplaySocketPool, ReplayRandom
from circuitmatter.utility.mdns import DummyMDNS
from circuitmatter.utility.mdns.avahi import Avahi
class NeoPixel(extended_color.ExtendedColorLight):
@property
def color_rgb(self):
return self._color
@color_rgb.setter
def color_rgb(self, value):
self._color = value
print(f"new color 0x{value:06x}")
@property
def brightness(self):
return self._brightness
@brightness.setter
def brightness(self, value):
self._brightness = value
print(f"new brightness {value}")
def on(self):
print("on!")
def off(self):
print("off!")
def run(replay_file=None):
device_state = pathlib.Path("live-rgb-device-state.json")
if replay_file:
replay_lines = []
with open(replay_file, "r") as f:
device_state_fn = f.readline().strip()
for line in f:
replay_lines.append(json.loads(line))
socketpool = ReplaySocketPool(replay_lines)
mdns_server = DummyMDNS()
random_source = ReplayRandom(replay_lines)
# Reset device state to before the captured run
if device_state_fn == "none":
device_state.unlink(missing_ok=True)
else:
device_state.write_text(pathlib.Path(device_state_fn).read_text())
else:
timestamp = time.strftime("%Y%m%d-%H%M%S")
record_file = open(f"test_data/recorded_packets-{timestamp}.jsonl", "w")
device_state_fn = f"test_data/device_state-{timestamp}.json"
replay_device_state = pathlib.Path(device_state_fn)
if device_state.exists():
record_file.write(f"{device_state_fn}\n")
# Save device state before we run so replays can use it.
replay_device_state.write_text(device_state.read_text())
else:
# No starting state.
record_file.write("none\n")
socketpool = RecordingSocketPool(record_file, socket)
mdns_server = Avahi()
random_source = RecordingRandom(record_file, random)
matter = cm.CircuitMatter(socketpool, mdns_server, random_source, device_state)
led = NeoPixel("neopixel1")
matter.add_device(led)
while True:
matter.process_packets()
if __name__ == "__main__":
import sys
replay_file = None
if len(sys.argv) > 1:
replay_file = sys.argv[1]
run(replay_file=replay_file)

29
examples/two_onoff_led.py Normal file
View file

@ -0,0 +1,29 @@
"""Simple LED on and off as a light."""
import circuitmatter as cm
from circuitmatter.device_types.lighting import on_off
import digitalio
import board
class LED(on_off.OnOffLight):
def __init__(self, name, led):
super().__init__(name)
self._led = led
self._led.direction = digitalio.Direction.OUTPUT
def on(self):
self._led.value = True
def off(self):
self._led.value = False
matter = cm.CircuitMatter()
led = LED("led1", digitalio.DigitalInOut(board.D19))
matter.add_device(led)
led = LED("led2", digitalio.DigitalInOut(board.D20))
matter.add_device(led)
while True:
matter.process_packets()