From c61da984c9491b465e3f54bc6ceb9b4a725feb56 Mon Sep 17 00:00:00 2001 From: Darell Tan Date: Sun, 25 Jun 2023 02:20:01 +0800 Subject: [PATCH] Add support for dimmers and dimmable bulbs The exposes entry looks similar to a switch, except the type is a "light" and it has `state` and `brightness`. Tested only on single channel dimmers. Also added a PercentageTranslator to translate between the HomeKit `brightness`, which is a percentage, to/from an arbitrary numeric value in Z2M. --- README.md | 1 + s_light.go | 50 ++++++++++++++++++++++++++++++++++++++++++++++++++ z2m.go | 47 +++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 98 insertions(+) create mode 100644 s_light.go diff --git a/README.md b/README.md index 9f6c85d..ba74ef8 100644 --- a/README.md +++ b/README.md @@ -15,6 +15,7 @@ Currently only supports the types of Zigbee devices I have: - Contact sensors - Motion sensors - Wall switch +- Dimmer, or dimmable light bulbs If you do use this software, note that it's in development and may contains bugs, or may even burn your house down. I offer no warranty, but you are welcome to file bugs. diff --git a/s_light.go b/s_light.go new file mode 100644 index 0000000..4b86b64 --- /dev/null +++ b/s_light.go @@ -0,0 +1,50 @@ +package hapz2m + +import ( + "github.com/brutella/hap/accessory" + "github.com/brutella/hap/characteristic" + "github.com/brutella/hap/service" +) + +func createLightServices(dev *Device) (byte, []*service.S, []*ExposeMapping, error) { + var svcs []*service.S + var exposes []*ExposeMapping + + for _, exp := range dev.Definition.Exposes { + if exp.Ignored() { + continue + } + + switch { + case exp.Type == "light" && len(exp.Features) > 0: + exp := exp // create a copy + + light := service.NewLightbulb() + + for _, feat := range exp.Features { + if !feat.IsStateSetGet() { + continue + } + + feat := feat + + switch { + case feat.Name == "state" && feat.Type == "binary": + svcs = append(svcs, light.S) + exposes = append(exposes, &ExposeMapping{&feat, light.On.C, nil}) + + case feat.Name == "brightness" && feat.Type == "numeric": + brightness := characteristic.NewBrightness() + light.AddC(brightness.C) + exposes = append(exposes, &ExposeMapping{&feat, brightness.C, nil}) + } + } + } + } + + return accessory.TypeLightbulb, svcs, exposes, nil +} + +func init() { + RegisterCreateServiceHandler(createLightServices) +} diff --git a/z2m.go b/z2m.go index e063a88..552bcb0 100644 --- a/z2m.go +++ b/z2m.go @@ -124,6 +124,17 @@ func createAccessory(dev *Device) (*accessory.A, []*ExposeMapping, error) { continue } + // if it's a percentage, then don't copy + if e.Characteristic.Unit == characteristic.UnitPercentage { + // assign a PercentageTranslator here, if there wasn't already + if e.Translator == nil && e.ExposesEntry.IsSettable() && + e.ExposesEntry.ValueMin != nil && e.ExposesEntry.ValueMax != nil { + + e.Translator = &PercentageTranslator{*e.ExposesEntry.ValueMin, *e.ExposesEntry.ValueMax} + } + continue + } + err := e.ExposesEntry.CopyValueRanges(e.Characteristic) if err != nil { return nil, nil, fmt.Errorf("cant copy value ranges for %s to cfmt %s: %s", @@ -223,6 +234,42 @@ func (t *BoolTranslator) ToCharacteristicValue(eVal any) (any, error) { return t.FalseValue, nil } +// Translates a numeric type exposed value to percentage Characteristic values +type PercentageTranslator struct{ Min, Max float64 } + +func (t *PercentageTranslator) ToExposedValue(cVal any) (any, error) { + cVal2, ok := valToFloat64(cVal) + if !ok { + return nil, ErrTranslationError + } + v := t.Min + (cVal2 / 100. * (t.Max - t.Min)) + return v, nil +} + +func (t *PercentageTranslator) ToCharacteristicValue(eVal any) (any, error) { + eVal2, ok := valToFloat64(eVal) + if !ok { + return nil, ErrTranslationError + } + v := (eVal2 - t.Min) * 100. / (t.Max - t.Min) + return v, nil +} + +// Converts numeric values to float64, if possible +// Returns the converted float64 value and a bool indicating if it was successful. +func valToFloat64(v any) (float64, bool) { + val := reflect.ValueOf(v) + switch { + case val.CanInt(): + return float64(val.Int()), true + case val.CanUint(): + return float64(val.Uint()), true + case val.CanFloat(): + return val.Float(), true + } + return 0, false +} + ////////////////////////////// // Maps a zigbee2mqtt device property into a HAP characteristic.