From 6a1fdd8f139a0b6855209a11e3ccaaa48d98eaef Mon Sep 17 00:00:00 2001 From: Peter Wood Date: Tue, 4 Jun 2024 22:07:03 +0100 Subject: [PATCH] Complete implementation of ZCLPowerSupply. --- attribute/monitor.go | 5 +- attribute/monitor_test.go | 27 + enumerate_device.go | 7 + implcaps/factory/mapping.go | 5 + implcaps/interface.go | 3 + implcaps/mock.go | 5 + implcaps/zcl/humidity_sensor/impl.go | 4 +- implcaps/zcl/humidity_sensor/impl_test.go | 22 +- implcaps/zcl/identify/impl.go | 15 +- implcaps/zcl/identify/impl_test.go | 26 +- implcaps/zcl/power_supply/impl.go | 487 +++++++++++++++++ implcaps/zcl/power_supply/impl_test.go | 533 +++++++++++++++++++ implcaps/zcl/pressure_sensor/impl.go | 4 +- implcaps/zcl/pressure_sensor/impl_test.go | 22 +- implcaps/zcl/temperature_sensor/impl.go | 4 +- implcaps/zcl/temperature_sensor/impl_test.go | 22 +- node.go | 2 +- rules/zcl.json | 4 +- zda_interface.go | 5 + 19 files changed, 1089 insertions(+), 113 deletions(-) create mode 100644 implcaps/zcl/power_supply/impl.go create mode 100644 implcaps/zcl/power_supply/impl_test.go diff --git a/attribute/monitor.go b/attribute/monitor.go index 4a7dbec..4f68b77 100644 --- a/attribute/monitor.go +++ b/attribute/monitor.go @@ -252,7 +252,8 @@ func (z *zclMonitor) zclFilter(a zigbee.IEEEAddress, _ zigbee.ApplicationMessage return a == z.ieeeAddress && m.SourceEndpoint == z.remoteEndpoint && m.DestinationEndpoint == z.localEndpoint && - m.Direction == zcl.ServerToClient + m.Direction == zcl.ServerToClient && + m.ClusterID == z.clusterID } func (z *zclMonitor) zclMessage(m communicator.MessageWithSource) { @@ -265,7 +266,7 @@ func (z *zclMonitor) zclMessage(m communicator.MessageWithSource) { } case *global.ReadAttributesResponse: for _, record := range cmd.Records { - if record.Identifier == z.attributeID && record.DataTypeValue.DataType == z.attributeDataType && record.Status == 0 { + if record.Status == 0 && record.Identifier == z.attributeID && record.DataTypeValue.DataType == z.attributeDataType { z.callback(record.Identifier, *record.DataTypeValue) } } diff --git a/attribute/monitor_test.go b/attribute/monitor_test.go index 136e359..d2d2651 100644 --- a/attribute/monitor_test.go +++ b/attribute/monitor_test.go @@ -452,11 +452,13 @@ func Test_zclMonitor_zclFilter(t *testing.T) { z.ieeeAddress = zigbee.GenerateLocalAdministeredIEEEAddress() z.localEndpoint = 1 z.remoteEndpoint = 2 + z.clusterID = 3 match := z.zclFilter(z.ieeeAddress, zigbee.ApplicationMessage{}, zcl.Message{ SourceEndpoint: z.remoteEndpoint, DestinationEndpoint: z.localEndpoint, Direction: zcl.ServerToClient, + ClusterID: 3, }) assert.True(t, match) @@ -467,11 +469,13 @@ func Test_zclMonitor_zclFilter(t *testing.T) { z.ieeeAddress = zigbee.GenerateLocalAdministeredIEEEAddress() z.localEndpoint = 1 z.remoteEndpoint = 2 + z.clusterID = 3 match := z.zclFilter(zigbee.GenerateLocalAdministeredIEEEAddress(), zigbee.ApplicationMessage{}, zcl.Message{ SourceEndpoint: z.remoteEndpoint, DestinationEndpoint: z.localEndpoint, Direction: zcl.ServerToClient, + ClusterID: 3, }) assert.False(t, match) @@ -482,11 +486,13 @@ func Test_zclMonitor_zclFilter(t *testing.T) { z.ieeeAddress = zigbee.GenerateLocalAdministeredIEEEAddress() z.localEndpoint = 1 z.remoteEndpoint = 2 + z.clusterID = 3 match := z.zclFilter(z.ieeeAddress, zigbee.ApplicationMessage{}, zcl.Message{ SourceEndpoint: 99, DestinationEndpoint: z.localEndpoint, Direction: zcl.ServerToClient, + ClusterID: 3, }) assert.False(t, match) @@ -497,11 +503,13 @@ func Test_zclMonitor_zclFilter(t *testing.T) { z.ieeeAddress = zigbee.GenerateLocalAdministeredIEEEAddress() z.localEndpoint = 1 z.remoteEndpoint = 2 + z.clusterID = 3 match := z.zclFilter(z.ieeeAddress, zigbee.ApplicationMessage{}, zcl.Message{ SourceEndpoint: z.remoteEndpoint, DestinationEndpoint: 99, Direction: zcl.ServerToClient, + ClusterID: 3, }) assert.False(t, match) @@ -512,11 +520,30 @@ func Test_zclMonitor_zclFilter(t *testing.T) { z.ieeeAddress = zigbee.GenerateLocalAdministeredIEEEAddress() z.localEndpoint = 1 z.remoteEndpoint = 2 + z.clusterID = 3 match := z.zclFilter(z.ieeeAddress, zigbee.ApplicationMessage{}, zcl.Message{ SourceEndpoint: z.remoteEndpoint, DestinationEndpoint: z.localEndpoint, Direction: zcl.ClientToServer, + ClusterID: 3, + }) + + assert.False(t, match) + }) + + t.Run("returns false if cluster doesn't match", func(t *testing.T) { + z := zclMonitor{} + z.ieeeAddress = zigbee.GenerateLocalAdministeredIEEEAddress() + z.localEndpoint = 1 + z.remoteEndpoint = 2 + z.clusterID = 1 + + match := z.zclFilter(z.ieeeAddress, zigbee.ApplicationMessage{}, zcl.Message{ + SourceEndpoint: z.remoteEndpoint, + DestinationEndpoint: z.localEndpoint, + Direction: zcl.ServerToClient, + ClusterID: 3, }) assert.False(t, match) diff --git a/enumerate_device.go b/enumerate_device.go index 1050079..5bcc7c4 100644 --- a/enumerate_device.go +++ b/enumerate_device.go @@ -14,6 +14,7 @@ import ( "github.com/shimmeringbee/zda/implcaps/factory" "github.com/shimmeringbee/zda/rules" "github.com/shimmeringbee/zigbee" + "runtime/debug" "slices" "sort" "sync" @@ -61,6 +62,7 @@ func (e enumerateDevice) startEnumeration(ctx context.Context, n *node) error { } go e.enumerate(ctx, n) + return nil } @@ -419,6 +421,11 @@ func (e enumerateDevice) enumerateCapabilityOnDevice(ctx context.Context, d *dev } e.logger.LogInfo(ctx, "Attaching capability implementation.") + defer func() { + if r := recover(); r != nil { + e.logger.LogPanic(ctx, "Capability paniced during enumeration!", logwrap.Datum("Panic", r), logwrap.Datum("Trace", string(debug.Stack()))) + } + }() attached, err := c.Enumerate(ctx, settings) if err != nil { e.logger.LogWarn(ctx, "Errored while attaching new capability.", logwrap.Err(err), logwrap.Datum("Attached", attached)) diff --git a/implcaps/factory/mapping.go b/implcaps/factory/mapping.go index 89353f2..a42b9c0 100644 --- a/implcaps/factory/mapping.go +++ b/implcaps/factory/mapping.go @@ -7,6 +7,7 @@ import ( "github.com/shimmeringbee/zda/implcaps/generic" "github.com/shimmeringbee/zda/implcaps/zcl/humidity_sensor" "github.com/shimmeringbee/zda/implcaps/zcl/identify" + "github.com/shimmeringbee/zda/implcaps/zcl/power_supply" "github.com/shimmeringbee/zda/implcaps/zcl/pressure_sensor" "github.com/shimmeringbee/zda/implcaps/zcl/temperature_sensor" ) @@ -16,6 +17,7 @@ const ZCLTemperatureSensor = "ZCLTemperatureSensor" const ZCLHumiditySensor = "ZCLHumiditySensor" const ZCLPressureSensor = "ZCLPressureSensor" const ZCLIdentify = "ZCLIdentify" +const ZCLPowerSupply = "ZCLPowerSupply" var Mapping = map[string]da.Capability{ GenericProductInformation: capabilities.ProductInformationFlag, @@ -23,6 +25,7 @@ var Mapping = map[string]da.Capability{ ZCLHumiditySensor: capabilities.RelativeHumiditySensorFlag, ZCLPressureSensor: capabilities.PressureSensorFlag, ZCLIdentify: capabilities.IdentifyFlag, + ZCLPowerSupply: capabilities.PowerSupplyFlag, } func Create(name string, iface implcaps.ZDAInterface) implcaps.ZDACapability { @@ -37,6 +40,8 @@ func Create(name string, iface implcaps.ZDAInterface) implcaps.ZDACapability { return pressure_sensor.NewPressureSensor(iface) case ZCLIdentify: return identify.NewIdentify(iface) + case ZCLPowerSupply: + return power_suply.NewPowerSupply(iface) default: return nil } diff --git a/implcaps/interface.go b/implcaps/interface.go index 4e33034..7c8f184 100644 --- a/implcaps/interface.go +++ b/implcaps/interface.go @@ -3,6 +3,7 @@ package implcaps import ( "context" "github.com/shimmeringbee/da" + "github.com/shimmeringbee/logwrap" "github.com/shimmeringbee/persistence" "github.com/shimmeringbee/zcl" "github.com/shimmeringbee/zcl/communicator" @@ -60,4 +61,6 @@ type ZDAInterface interface { ZCLRegister(func(*zcl.CommandRegistry)) //TransmissionLookup resolves destination information for a capability. TransmissionLookup(da.Device, zigbee.ProfileID) (zigbee.IEEEAddress, zigbee.Endpoint, bool, uint8) + //Logger returns a logger to be used. + Logger() logwrap.Logger } diff --git a/implcaps/mock.go b/implcaps/mock.go index 6e6e50f..982ddab 100644 --- a/implcaps/mock.go +++ b/implcaps/mock.go @@ -2,6 +2,7 @@ package implcaps import ( "github.com/shimmeringbee/da" + "github.com/shimmeringbee/logwrap" "github.com/shimmeringbee/zcl" "github.com/shimmeringbee/zcl/communicator" "github.com/shimmeringbee/zda/attribute" @@ -13,6 +14,10 @@ type MockZDAInterface struct { mock.Mock } +func (m *MockZDAInterface) Logger() logwrap.Logger { + return m.Called().Get(0).(logwrap.Logger) +} + func (m *MockZDAInterface) ZCLRegister(f func(*zcl.CommandRegistry)) { m.Called(f) } diff --git a/implcaps/zcl/humidity_sensor/impl.go b/implcaps/zcl/humidity_sensor/impl.go index d2aa336..2e32b6d 100644 --- a/implcaps/zcl/humidity_sensor/impl.go +++ b/implcaps/zcl/humidity_sensor/impl.go @@ -57,8 +57,6 @@ func (i *Implementation) Load(ctx context.Context) (bool, error) { func (i *Implementation) Enumerate(ctx context.Context, m map[string]any) (bool, error) { endpoint := implcaps.Get(m, "ZigbeeEndpoint", zigbee.Endpoint(1)) - clusterId := implcaps.Get(m, "ZigbeeHumidityMeasurementClusterID", zcl.RelativeHumidityMeasurementId) - attributeId := implcaps.Get(m, "ZigbeeHumidityMeasurementAttributeID", relative_humidity_measurement.MeasuredValue) reporting := attribute.ReportingConfig{ Mode: attribute.AttemptConfigureReporting, @@ -72,7 +70,7 @@ func (i *Implementation) Enumerate(ctx context.Context, m map[string]any) (bool, Interval: 1 * time.Minute, } - if err := i.am.Attach(ctx, endpoint, clusterId, attributeId, zcl.TypeUnsignedInt16, reporting, polling); err != nil { + if err := i.am.Attach(ctx, endpoint, zcl.RelativeHumidityMeasurementId, relative_humidity_measurement.MeasuredValue, zcl.TypeUnsignedInt16, reporting, polling); err != nil { return false, err } diff --git a/implcaps/zcl/humidity_sensor/impl_test.go b/implcaps/zcl/humidity_sensor/impl_test.go index 66158c3..9f2c9de 100644 --- a/implcaps/zcl/humidity_sensor/impl_test.go +++ b/implcaps/zcl/humidity_sensor/impl_test.go @@ -82,7 +82,7 @@ func TestImplementation_Load(t *testing.T) { } func TestImplementation_Enumerate(t *testing.T) { - t.Run("attaches to the attribute monitor, using default attributes", func(t *testing.T) { + t.Run("attaches to the attribute monitor", func(t *testing.T) { mm := &attribute.MockMonitor{} defer mm.AssertExpectations(t) @@ -96,26 +96,6 @@ func TestImplementation_Enumerate(t *testing.T) { assert.NoError(t, err) }) - t.Run("attaches to the attribute monitor, using overridden attributes", func(t *testing.T) { - mm := &attribute.MockMonitor{} - defer mm.AssertExpectations(t) - - mm.On("Attach", mock.Anything, zigbee.Endpoint(0x02), zigbee.ClusterID(0x500), zcl.AttributeID(0x10), zcl.TypeUnsignedInt16, mock.Anything, mock.Anything).Return(nil) - - i := NewHumiditySensor(nil) - i.am = mm - - attributes := map[string]any{ - "ZigbeeEndpoint": zigbee.Endpoint(0x02), - "ZigbeeHumidityMeasurementClusterID": zigbee.ClusterID(0x500), - "ZigbeeHumidityMeasurementAttributeID": zcl.AttributeID(0x10), - } - attached, err := i.Enumerate(context.TODO(), attributes) - - assert.True(t, attached) - assert.NoError(t, err) - }) - t.Run("fails if attach to the attribute monitor fails", func(t *testing.T) { mm := &attribute.MockMonitor{} defer mm.AssertExpectations(t) diff --git a/implcaps/zcl/identify/impl.go b/implcaps/zcl/identify/impl.go index 9f7e1f6..17d8877 100644 --- a/implcaps/zcl/identify/impl.go +++ b/implcaps/zcl/identify/impl.go @@ -35,7 +35,6 @@ type Implementation struct { am attribute.Monitor zi implcaps.ZDAInterface - clusterId zigbee.ClusterID remoteEndpoint zigbee.Endpoint timerMutex *sync.Mutex @@ -66,13 +65,6 @@ func (i *Implementation) Load(ctx context.Context) (bool, error) { return false, fmt.Errorf("monitor missing config parameter: %s", implcaps.RemoteEndpointKey) } - if v, ok := i.s.Int(implcaps.ClusterIdKey); ok { - i.clusterId = zigbee.ClusterID(v) - } else { - // i.logger.Error(ctx, "Required config parameter missing.", logwrap.Datum("name", implcaps.ClusterIdKey)) - return false, fmt.Errorf("monitor missing config parameter: %s", implcaps.ClusterIdKey) - } - if err := i.am.Load(ctx); err != nil { return false, err } else { @@ -82,10 +74,7 @@ func (i *Implementation) Load(ctx context.Context) (bool, error) { func (i *Implementation) Enumerate(ctx context.Context, m map[string]any) (bool, error) { i.remoteEndpoint = implcaps.Get(m, "ZigbeeEndpoint", zigbee.Endpoint(1)) - i.clusterId = implcaps.Get(m, "ZigbeeIdentifyClusterID", zcl.IdentifyId) - attributeId := implcaps.Get(m, "ZigbeeIdentifyDurationAttributeID", identify.IdentifyTime) - i.s.Set(implcaps.ClusterIdKey, int(i.clusterId)) i.s.Set(implcaps.RemoteEndpointKey, int(i.remoteEndpoint)) reporting := attribute.ReportingConfig{ @@ -100,7 +89,7 @@ func (i *Implementation) Enumerate(ctx context.Context, m map[string]any) (bool, Interval: 1 * time.Minute, } - if err := i.am.Attach(ctx, i.remoteEndpoint, i.clusterId, attributeId, zcl.TypeUnsignedInt16, reporting, polling); err != nil { + if err := i.am.Attach(ctx, i.remoteEndpoint, zcl.IdentifyId, identify.IdentifyTime, zcl.TypeUnsignedInt16, reporting, polling); err != nil { return false, err } @@ -213,7 +202,7 @@ func (i *Implementation) Identify(ctx context.Context, duration time.Duration) e Direction: zcl.ClientToServer, TransactionSequence: seq, Manufacturer: zigbee.NoManufacturer, - ClusterID: i.clusterId, + ClusterID: zcl.IdentifyId, SourceEndpoint: localEndpoint, DestinationEndpoint: i.remoteEndpoint, CommandIdentifier: identify.IdentifyId, diff --git a/implcaps/zcl/identify/impl_test.go b/implcaps/zcl/identify/impl_test.go index 93a2c71..709b012 100644 --- a/implcaps/zcl/identify/impl_test.go +++ b/implcaps/zcl/identify/impl_test.go @@ -89,7 +89,7 @@ func TestImplementation_Load(t *testing.T) { } func TestImplementation_Enumerate(t *testing.T) { - t.Run("attaches to the attribute monitor, using default attributes", func(t *testing.T) { + t.Run("attaches to the attribute monitor", func(t *testing.T) { mm := &attribute.MockMonitor{} defer mm.AssertExpectations(t) @@ -104,27 +104,6 @@ func TestImplementation_Enumerate(t *testing.T) { assert.NoError(t, err) }) - t.Run("attaches to the attribute monitor, using overridden attributes", func(t *testing.T) { - mm := &attribute.MockMonitor{} - defer mm.AssertExpectations(t) - - mm.On("Attach", mock.Anything, zigbee.Endpoint(0x02), zigbee.ClusterID(0x500), zcl.AttributeID(0x10), zcl.TypeUnsignedInt16, mock.Anything, mock.Anything).Return(nil) - - i := &Implementation{} - i.am = mm - i.s = memory.New() - - attributes := map[string]any{ - "ZigbeeEndpoint": zigbee.Endpoint(0x02), - "ZigbeeIdentifyClusterID": zigbee.ClusterID(0x500), - "ZigbeeIdentifyDurationAttributeID": zcl.AttributeID(0x10), - } - attached, err := i.Enumerate(context.TODO(), attributes) - - assert.True(t, attached) - assert.NoError(t, err) - }) - t.Run("fails if attach to the attribute monitor fails", func(t *testing.T) { mm := &attribute.MockMonitor{} defer mm.AssertExpectations(t) @@ -282,7 +261,6 @@ func TestImplementation_Identify(t *testing.T) { mzi.On("TransmissionLookup", md, zigbee.ProfileHomeAutomation).Return(ieee, localEndpoint, false, seq) - i.clusterId = zcl.IdentifyId i.remoteEndpoint = 4 expectedMsg := zcl.Message{ @@ -290,7 +268,7 @@ func TestImplementation_Identify(t *testing.T) { Direction: zcl.ClientToServer, TransactionSequence: uint8(seq), Manufacturer: zigbee.NoManufacturer, - ClusterID: i.clusterId, + ClusterID: zcl.IdentifyId, SourceEndpoint: localEndpoint, DestinationEndpoint: i.remoteEndpoint, CommandIdentifier: identify.IdentifyId, diff --git a/implcaps/zcl/power_supply/impl.go b/implcaps/zcl/power_supply/impl.go new file mode 100644 index 0000000..313e517 --- /dev/null +++ b/implcaps/zcl/power_supply/impl.go @@ -0,0 +1,487 @@ +package power_suply + +import ( + "context" + "fmt" + "github.com/shimmeringbee/da" + "github.com/shimmeringbee/da/capabilities" + "github.com/shimmeringbee/logwrap" + "github.com/shimmeringbee/persistence" + "github.com/shimmeringbee/persistence/converter" + "github.com/shimmeringbee/zcl" + "github.com/shimmeringbee/zcl/commands/local/basic" + "github.com/shimmeringbee/zcl/commands/local/power_configuration" + "github.com/shimmeringbee/zda/attribute" + "github.com/shimmeringbee/zda/implcaps" + "github.com/shimmeringbee/zigbee" + "math" + "time" +) + +var _ capabilities.PowerSupply = (*Implementation)(nil) +var _ capabilities.WithLastChangeTime = (*Implementation)(nil) +var _ capabilities.WithLastUpdateTime = (*Implementation)(nil) +var _ implcaps.ZDACapability = (*Implementation)(nil) + +const MainsPresentKey = "MainsPresent" + +const MainsVoltageKey = "MainsVoltage" +const MainsFrequencyKey = "MainsFrequency" + +var BatteryVoltage = func(n int) string { return fmt.Sprintf("BatteryVoltage%d", n) } +var BatteryPercentage = func(n int) string { return fmt.Sprintf("BatteryPercentage%d", n) } + +const MainsVoltagePresentKey = "MainsVoltagePresent" +const MainsFrequencyPresentKey = "MainsFrequencyPresent" + +var BatteryPresent = func(n int) string { return fmt.Sprintf("BatteryPresent%d", n) } + +var BatteryVoltagePresent = func(n int) string { return fmt.Sprintf("BatteryVoltage%dPresent", n) } +var BatteryPercentagePresent = func(n int) string { return fmt.Sprintf("BatteryPercentage%dPresent", n) } + +func NewPowerSupply(zi implcaps.ZDAInterface) *Implementation { + return &Implementation{zi: zi, l: zi.Logger()} +} + +type Implementation struct { + s persistence.Section + d da.Device + zi implcaps.ZDAInterface + l logwrap.Logger + + remoteEndpoint zigbee.Endpoint + + mainsVoltageMonitor attribute.Monitor + mainsFrequencyMonitor attribute.Monitor + batteryVoltageMonitor [3]attribute.Monitor + batteryPercentageMonitor [3]attribute.Monitor + + mainsPresent bool + mainsVoltagePresent bool + mainsFrequencyPresent bool + batteryPresent [3]bool + batteryVoltagePresent [3]bool + batteryPercentagePresent [3]bool +} + +func (i *Implementation) Capability() da.Capability { + return capabilities.PowerSupplyFlag +} + +func (i *Implementation) Name() string { + return capabilities.StandardNames[capabilities.PowerSupplyFlag] +} + +func (i *Implementation) Init(d da.Device, s persistence.Section) { + i.d = d + i.s = s + + i.mainsVoltageMonitor = i.zi.NewAttributeMonitor() + i.mainsVoltageMonitor.Init(s.Section("AttributeMonitor", "MainsVoltage"), d, i.update) + i.mainsFrequencyMonitor = i.zi.NewAttributeMonitor() + i.mainsFrequencyMonitor.Init(s.Section("AttributeMonitor", "MainsFrequency"), d, i.update) + + for n := range len(i.batteryVoltageMonitor) { + i.batteryVoltageMonitor[n] = i.zi.NewAttributeMonitor() + i.batteryVoltageMonitor[n].Init(s.Section("AttributeMonitor", fmt.Sprintf("BatteryVoltage%d", n)), d, i.update) + i.batteryPercentageMonitor[n] = i.zi.NewAttributeMonitor() + i.batteryPercentageMonitor[n].Init(s.Section("AttributeMonitor", fmt.Sprintf("BatteryPercentage%d", n)), d, i.update) + } +} + +func (i *Implementation) Load(ctx context.Context) (bool, error) { + i.mainsPresent, _ = i.s.Bool(MainsPresentKey) + i.mainsVoltagePresent, _ = i.s.Bool(MainsVoltagePresentKey) + i.mainsFrequencyPresent, _ = i.s.Bool(MainsFrequencyPresentKey) + + if i.mainsVoltagePresent { + if err := i.mainsVoltageMonitor.Load(ctx); err != nil { + i.l.Warn(ctx, "Failed to attach mains voltage monitor.", logwrap.Err(err)) + return false, fmt.Errorf("mains voltage monitor, attach failed: %w", err) + } + } + + if i.mainsFrequencyPresent { + if err := i.mainsFrequencyMonitor.Load(ctx); err != nil { + i.l.Warn(ctx, "Failed to attach mains frequency monitor.", logwrap.Err(err)) + return false, fmt.Errorf("mains voltage frequency, attach failed: %w", err) + } + } + + for n := range 3 { + i.batteryPresent[n], _ = i.s.Bool(BatteryPresent(n)) + i.batteryPercentagePresent[n], _ = i.s.Bool(BatteryPercentagePresent(n)) + i.batteryVoltagePresent[n], _ = i.s.Bool(BatteryVoltagePresent(n)) + + if i.batteryPercentagePresent[n] { + if err := i.batteryPercentageMonitor[n].Load(ctx); err != nil { + i.l.Warn(ctx, "Failed to attach battery percentage monitor.", logwrap.Err(err), logwrap.Datum("Battery", n)) + return false, fmt.Errorf("battery %d percentage monitor, attach failed: %w", n, err) + } + } + + if i.batteryVoltagePresent[n] { + if err := i.batteryVoltageMonitor[n].Load(ctx); err != nil { + i.l.Warn(ctx, "Failed to attach battery voltage monitor.", logwrap.Err(err), logwrap.Datum("Battery", n)) + return false, fmt.Errorf("battery %d voltage monitor, attach failed: %w", n, err) + } + } + } + + return true, nil +} + +func (i *Implementation) enumerateBasicCluster(pctx context.Context) (bool, error) { + var lastError error + attach := false + + ctx, done := context.WithTimeout(pctx, 1*time.Second) + defer done() + + ieee, localEndpoint, ack, seq := i.zi.TransmissionLookup(i.d, zigbee.ProfileHomeAutomation) + i.l.Info(ctx, "Reading basic power configuration data.") + if resp, err := i.zi.ZCLCommunicator().ReadAttributes(ctx, ieee, ack, zcl.BasicId, zigbee.NoManufacturer, localEndpoint, i.remoteEndpoint, seq, []zcl.AttributeID{basic.PowerSource}); err != nil { + lastError = err + i.l.Warn(ctx, "Failed to read basic power configuration.", logwrap.Err(err)) + } else if len(resp) == 0 || resp[0].Status != 0 || resp[0].Identifier != basic.PowerSource { + i.l.Warn(ctx, "Device did not respond to read attribute for mandatory PowerSource attribute!") + } else { + val64, ok64 := resp[0].DataTypeValue.Value.(uint64) + val8, ok8 := resp[0].DataTypeValue.Value.(uint8) + + val := val64 + uint64(val8) + + if ok8 || ok64 { + attach = true + + switch val & 0x0f { + case 0x01, 0x02, 0x04, 0x05, 0x06: + i.mainsPresent = true + case 0x03: + i.batteryPresent[0] = true + } + + if val&0x80 == 0x80 { + if i.batteryPresent[0] == true { + i.batteryPresent[1] = true + } else { + i.batteryPresent[0] = true + } + } + } else { + i.l.Warn(ctx, "Device did not return int coercible value.", logwrap.Datum("DataType", resp[0].DataTypeValue.DataType)) + } + } + + if i.mainsPresent { + i.s.Set(MainsPresentKey, true) + } + + for n := range 2 { + if i.batteryPresent[n] { + i.s.Set(BatteryPresent(n), true) + } + } + + return attach, lastError +} + +func (i *Implementation) enumeratePowerConfigurationCluster(pctx context.Context) (bool, error) { + var lastError error + attach := false + + ieee, localEndpoint, ack, seq := i.zi.TransmissionLookup(i.d, zigbee.ProfileHomeAutomation) + ctx, done := context.WithTimeout(pctx, 5*time.Second) + i.l.Info(ctx, "Reading PowerConfiguration cluster for mains data.") + if resp, err := i.zi.ZCLCommunicator().ReadAttributes(ctx, ieee, ack, zcl.PowerConfigurationId, zigbee.NoManufacturer, localEndpoint, i.remoteEndpoint, seq, []zcl.AttributeID{ + power_configuration.MainsVoltage, + power_configuration.MainsFrequency, + }); err != nil { + lastError = err + i.l.Warn(ctx, "Errored reading mains from power configuration.", logwrap.Err(err)) + } else { + for _, d := range resp { + if d.Status == 0 { + attach = true + + switch d.Identifier { + case power_configuration.MainsVoltage: + i.mainsPresent = true + i.mainsVoltagePresent = true + case power_configuration.MainsFrequency: + i.mainsPresent = true + i.mainsFrequencyPresent = true + } + } + } + } + done() + + batteryPercentageAttributes := []zcl.AttributeID{power_configuration.BatteryPercentageRemaining, power_configuration.BatterySource2PercentageRemaining, power_configuration.BatterySource3PercentageRemaining} + batteryVoltageAttributes := []zcl.AttributeID{power_configuration.BatteryVoltage, power_configuration.BatterySource2Voltage, power_configuration.BatterySource3Voltage} + + for n := range 3 { + _, _, _, seq = i.zi.TransmissionLookup(i.d, zigbee.ProfileHomeAutomation) + ctx, done := context.WithTimeout(pctx, 5*time.Second) + i.l.Info(ctx, "Reading PowerConfiguration cluster for battery data.", logwrap.Datum("Battery", n)) + if resp, err := i.zi.ZCLCommunicator().ReadAttributes(ctx, ieee, ack, zcl.PowerConfigurationId, zigbee.NoManufacturer, localEndpoint, i.remoteEndpoint, seq, []zcl.AttributeID{ + batteryVoltageAttributes[n], + batteryPercentageAttributes[n], + }); err != nil { + lastError = err + i.l.LogWarn(ctx, "Failed to query battery status.", logwrap.Err(err), logwrap.Datum("Battery", n)) + } else { + for _, d := range resp { + attach = true + + if d.Status == 0 { + switch d.Identifier { + case batteryPercentageAttributes[n]: + i.batteryPresent[n] = true + i.batteryPercentagePresent[n] = true + case batteryVoltageAttributes[n]: + i.batteryPresent[n] = true + i.batteryVoltagePresent[n] = true + } + } + } + } + done() + } + + i.s.Set(MainsPresentKey, i.mainsPresent) + + if i.mainsVoltagePresent { + i.s.Set(MainsVoltagePresentKey, true) + + if err := i.mainsVoltageMonitor.Attach(pctx, i.remoteEndpoint, zcl.PowerConfigurationId, power_configuration.MainsVoltage, zcl.TypeUnsignedInt16, attribute.ReportingConfig{Mode: attribute.AttemptConfigureReporting, MinimumInterval: 1 * time.Minute, MaximumInterval: 5 * time.Minute, ReportableChange: uint(5)}, attribute.PollingConfig{Mode: attribute.PollIfReportingFailed, Interval: 5 * time.Minute}); err != nil { + lastError = err + i.l.Warn(pctx, "Errored attaching mains voltage monitor.", logwrap.Err(err)) + } + } + + if i.mainsFrequencyPresent { + i.s.Set(MainsFrequencyPresentKey, true) + + if err := i.mainsVoltageMonitor.Attach(pctx, i.remoteEndpoint, zcl.PowerConfigurationId, power_configuration.MainsFrequency, zcl.TypeUnsignedInt8, attribute.ReportingConfig{Mode: attribute.AttemptConfigureReporting, MinimumInterval: 1 * time.Minute, MaximumInterval: 5 * time.Minute, ReportableChange: uint(1)}, attribute.PollingConfig{Mode: attribute.PollIfReportingFailed, Interval: 5 * time.Minute}); err != nil { + lastError = err + i.l.Warn(pctx, "Errored attaching mains frequency monitor.", logwrap.Err(err)) + } + } + + for n := range 3 { + i.s.Set(BatteryPresent(n), i.batteryPresent[n]) + + if i.batteryPercentagePresent[n] { + i.s.Set(BatteryPercentagePresent(n), true) + + if err := i.batteryPercentageMonitor[n].Attach(pctx, i.remoteEndpoint, zcl.PowerConfigurationId, batteryPercentageAttributes[n], zcl.TypeUnsignedInt8, attribute.ReportingConfig{Mode: attribute.AttemptConfigureReporting, MinimumInterval: 1 * time.Minute, MaximumInterval: 5 * time.Minute, ReportableChange: uint(1)}, attribute.PollingConfig{Mode: attribute.PollIfReportingFailed, Interval: 5 * time.Minute}); err != nil { + lastError = err + i.l.Warn(pctx, "Errored attaching battery percentage monitor.", logwrap.Err(err), logwrap.Datum("Battery", n)) + } + } + + if i.batteryVoltagePresent[n] { + i.s.Set(BatteryVoltagePresent(n), true) + + if err := i.batteryVoltageMonitor[n].Attach(pctx, i.remoteEndpoint, zcl.PowerConfigurationId, batteryVoltageAttributes[n], zcl.TypeUnsignedInt8, attribute.ReportingConfig{Mode: attribute.AttemptConfigureReporting, MinimumInterval: 1 * time.Minute, MaximumInterval: 5 * time.Minute, ReportableChange: uint(1)}, attribute.PollingConfig{Mode: attribute.PollIfReportingFailed, Interval: 5 * time.Minute}); err != nil { + lastError = err + i.l.Warn(pctx, "Errored attaching battery percentage monitor.", logwrap.Err(err), logwrap.Datum("Battery", n)) + } + } + } + + return attach, lastError +} + +func (i *Implementation) Enumerate(ctx context.Context, m map[string]any) (bool, error) { + var lastError error + attach := false + + i.remoteEndpoint = implcaps.Get(m, "ZigbeeEndpoint", zigbee.Endpoint(1)) + i.s.Set(implcaps.RemoteEndpointKey, int(i.remoteEndpoint)) + + if implcaps.Get(m, "ZigbeeBasicClusterPresent", false) { + if bcAttach, err := i.enumerateBasicCluster(ctx); err != nil { + lastError = fmt.Errorf("enumerating basic cluster: %w", err) + } else if bcAttach { + attach = true + } + } + + if implcaps.Get(m, "ZigbeePowerConfigurationClusterPresent", false) { + if pcAttach, err := i.enumeratePowerConfigurationCluster(ctx); err != nil { + lastError = fmt.Errorf("enumerating power configuration cluster: %w", err) + } else if pcAttach { + attach = true + } + } + + return attach, lastError +} + +func (i *Implementation) Detach(ctx context.Context, detachType implcaps.DetachType) error { + var lastError error + + if i.mainsVoltagePresent { + if err := i.mainsVoltageMonitor.Detach(ctx, detachType == implcaps.NoLongerEnumerated); err != nil { + lastError = err + i.l.Warn(ctx, "Failed to attach mains voltage monitor.", logwrap.Err(err)) + } + } + + if i.mainsFrequencyPresent { + if err := i.mainsFrequencyMonitor.Detach(ctx, detachType == implcaps.NoLongerEnumerated); err != nil { + lastError = err + i.l.Warn(ctx, "Failed to attach mains frequency monitor.", logwrap.Err(err)) + } + } + + for n := range 3 { + if i.batteryPercentagePresent[n] { + if err := i.batteryPercentageMonitor[n].Detach(ctx, detachType == implcaps.NoLongerEnumerated); err != nil { + lastError = err + i.l.Warn(ctx, "Failed to attach battery percentage monitor.", logwrap.Err(err), logwrap.Datum("Battery", n)) + } + } + + if i.batteryVoltagePresent[n] { + if err := i.batteryVoltageMonitor[n].Detach(ctx, detachType == implcaps.NoLongerEnumerated); err != nil { + lastError = err + i.l.Warn(ctx, "Failed to attach battery voltage monitor.", logwrap.Err(err), logwrap.Datum("Battery", n)) + } + } + } + + return lastError +} + +func (i *Implementation) ImplName() string { + return "ZCLPowerSupply" +} + +func (i *Implementation) LastUpdateTime(_ context.Context) (time.Time, error) { + t, _ := converter.Retrieve(i.s, implcaps.LastUpdatedKey, converter.TimeDecoder) + return t, nil +} + +func (i *Implementation) LastChangeTime(_ context.Context) (time.Time, error) { + t, _ := converter.Retrieve(i.s, implcaps.LastChangedKey, converter.TimeDecoder) + return t, nil +} + +func (i *Implementation) Status(_ context.Context) (capabilities.PowerState, error) { + ret := &capabilities.PowerState{ + Mains: nil, + Battery: nil, + } + + if i.mainsPresent { + var present capabilities.PowerStatusPresent + voltage := 0.0 + frequency := 0.0 + + if i.mainsVoltagePresent { + present |= capabilities.Voltage + voltage, _ = i.s.Float(MainsVoltageKey) + } + + if i.mainsFrequencyPresent { + present |= capabilities.Frequency + frequency, _ = i.s.Float(MainsFrequencyKey) + } + + ret.Mains = append(ret.Mains, capabilities.PowerMainsState{ + Voltage: voltage, + Frequency: frequency, + Available: false, + Present: present, + }) + } + + for n := range 3 { + if i.batteryPresent[n] { + var present capabilities.PowerStatusPresent + remaining := 0.0 + voltage := 0.0 + + if i.batteryPercentagePresent[n] { + present |= capabilities.Remaining + remaining, _ = i.s.Float(BatteryPercentage(n)) + remaining /= 100 + } + + if i.batteryVoltagePresent[n] { + present |= capabilities.Voltage + voltage, _ = i.s.Float(BatteryVoltage(n)) + } + + ret.Battery = append(ret.Battery, capabilities.PowerBatteryState{ + Voltage: voltage, + Remaining: remaining, + Available: false, + Present: present, + }) + } + } + + return *ret, nil +} + +func (i *Implementation) update(id zcl.AttributeID, value zcl.AttributeDataTypeValue) { + announce := false + + if raw, ok := value.Value.(uint64); ok { + switch id { + case power_configuration.MainsVoltage: + newVoltage := float64(raw) / 10.0 + currentVoltage, _ := i.s.Float(MainsVoltageKey) + + announce = math.Abs(newVoltage-currentVoltage) >= 0.1 + i.s.Set(MainsVoltageKey, newVoltage) + + case power_configuration.MainsFrequency: + newFrequency := float64(raw) / 2.0 + currentFrequency, _ := i.s.Float(MainsFrequencyKey) + + announce = math.Abs(newFrequency-currentFrequency) >= 1 + i.s.Set(MainsFrequencyKey, newFrequency) + + case power_configuration.BatteryVoltage, power_configuration.BatterySource2Voltage, power_configuration.BatterySource3Voltage: + battery := attributeIdToBattery(id) + newVoltage := float64(raw) / 10.0 + currentVoltage, _ := i.s.Float(BatteryVoltage(battery)) + + announce = math.Abs(newVoltage-currentVoltage) >= 0.05 + i.s.Set(BatteryVoltage(battery), newVoltage) + + case power_configuration.BatteryPercentageRemaining, power_configuration.BatterySource2PercentageRemaining, power_configuration.BatterySource3PercentageRemaining: + battery := attributeIdToBattery(id) + newPercentage := float64(raw) / 2 + currentPercentage, _ := i.s.Float(BatteryPercentage(battery)) + + announce = math.Abs(newPercentage-currentPercentage) >= 0.1 + i.s.Set(BatteryPercentage(battery), newPercentage) + } + } + + if announce { + converter.Store(i.s, implcaps.LastChangedKey, time.Now(), converter.TimeEncoder) + s, _ := i.Status(context.Background()) + i.zi.SendEvent(capabilities.PowerStatusUpdate{Device: i.d, PowerStatus: s}) + } + + converter.Store(i.s, implcaps.LastUpdatedKey, time.Now(), converter.TimeEncoder) +} + +func attributeIdToBattery(id zcl.AttributeID) int { + if id&0x0020 == 0x0020 { + return 0 + } else if id&0x0040 == 0x0040 { + return 1 + } else if id&0x0060 == 0x0060 { + return 2 + } + + return 0 +} diff --git a/implcaps/zcl/power_supply/impl_test.go b/implcaps/zcl/power_supply/impl_test.go new file mode 100644 index 0000000..86597ac --- /dev/null +++ b/implcaps/zcl/power_supply/impl_test.go @@ -0,0 +1,533 @@ +package power_suply + +import ( + "context" + "github.com/shimmeringbee/da/capabilities" + "github.com/shimmeringbee/logwrap" + "github.com/shimmeringbee/logwrap/impl/discard" + "github.com/shimmeringbee/persistence/converter" + "github.com/shimmeringbee/persistence/impl/memory" + "github.com/shimmeringbee/zcl" + "github.com/shimmeringbee/zcl/commands/global" + "github.com/shimmeringbee/zcl/commands/local/basic" + "github.com/shimmeringbee/zcl/commands/local/power_configuration" + "github.com/shimmeringbee/zda/attribute" + "github.com/shimmeringbee/zda/implcaps" + "github.com/shimmeringbee/zda/mocks" + "github.com/shimmeringbee/zigbee" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "io" + "testing" + "time" +) + +func TestImplementation_BaseFunctions(t *testing.T) { + t.Run("basic static functions respond correctly", func(t *testing.T) { + mzi := &implcaps.MockZDAInterface{} + defer mzi.AssertExpectations(t) + + mzi.On("Logger").Return(logwrap.New(discard.Discard())) + + i := NewPowerSupply(mzi) + + assert.Equal(t, capabilities.PowerSupplyFlag, i.Capability()) + assert.Equal(t, capabilities.StandardNames[capabilities.PowerSupplyFlag], i.Name()) + assert.Equal(t, "ZCLPowerSupply", i.ImplName()) + }) +} + +func TestImplementation_Init(t *testing.T) { + t.Run("constructs a new attribute monitor correctly initialising it", func(t *testing.T) { + mzi := &implcaps.MockZDAInterface{} + defer mzi.AssertExpectations(t) + + mzi.On("Logger").Return(logwrap.New(discard.Discard())) + + mm := &attribute.MockMonitor{} + defer mm.AssertExpectations(t) + + mzi.On("NewAttributeMonitor").Return(mm) + + md := &mocks.MockDevice{} + defer md.AssertExpectations(t) + + s := memory.New() + es := s.Section("AttributeMonitor", implcaps.ReadingKey) + + mm.On("Init", es, md, mock.Anything) + + i := NewPowerSupply(mzi) + i.Init(md, s) + }) +} + +func TestImplementation_Load(t *testing.T) { + t.Run("loads attribute monitor functionality, returning true if successful", func(t *testing.T) { + mzi := &implcaps.MockZDAInterface{} + defer mzi.AssertExpectations(t) + + mzi.On("Logger").Return(logwrap.New(discard.Discard())) + + mm := &attribute.MockMonitor{} + defer mm.AssertExpectations(t) + + mm.On("Load", mock.Anything).Return(nil) + + i := NewPowerSupply(mzi) + i.s = memory.New() + + i.s.Set(MainsVoltagePresentKey, true) + i.mainsVoltageMonitor = mm + attached, err := i.Load(context.TODO()) + + assert.True(t, attached) + assert.NoError(t, err) + }) + + t.Run("loads attribute monitor functionality, returning false if error", func(t *testing.T) { + mzi := &implcaps.MockZDAInterface{} + defer mzi.AssertExpectations(t) + + mzi.On("Logger").Return(logwrap.New(discard.Discard())) + + mm := &attribute.MockMonitor{} + defer mm.AssertExpectations(t) + + mm.On("Load", mock.Anything).Return(io.EOF) + + i := NewPowerSupply(mzi) + i.s = memory.New() + + i.s.Set(MainsVoltagePresentKey, true) + i.mainsVoltageMonitor = mm + attached, err := i.Load(context.TODO()) + + assert.False(t, attached) + assert.Error(t, err) + }) +} + +func TestImplementation_Enumerate(t *testing.T) { + t.Run("performed basic power source enumeration, if cluster present", func(t *testing.T) { + mzi := &implcaps.MockZDAInterface{} + defer mzi.AssertExpectations(t) + + mzi.On("Logger").Return(logwrap.New(discard.Discard())) + + ieeeAddress := zigbee.GenerateLocalAdministeredIEEEAddress() + mzi.On("TransmissionLookup", mock.Anything, zigbee.ProfileHomeAutomation).Return(ieeeAddress, zigbee.Endpoint(2), true, 4) + + mzc := &mocks.MockZCLCommunicator{} + defer mzc.AssertExpectations(t) + + mzi.On("ZCLCommunicator").Return(mzc) + + mzc.On("ReadAttributes", mock.Anything, ieeeAddress, true, zcl.BasicId, zigbee.NoManufacturer, zigbee.Endpoint(2), zigbee.Endpoint(1), uint8(4), []zcl.AttributeID{basic.PowerSource}). + Return([]global.ReadAttributeResponseRecord{{Identifier: basic.PowerSource, Status: 0, DataTypeValue: &zcl.AttributeDataTypeValue{Value: uint8(0x81)}}}, nil) + + i := NewPowerSupply(mzi) + i.s = memory.New() + + attached, err := i.Enumerate(context.Background(), map[string]any{"ZigbeeEndpoint": 1, "ZigbeeBasicClusterPresent": true}) + assert.NoError(t, err) + assert.True(t, attached) + + assert.True(t, i.mainsPresent) + assert.True(t, i.batteryPresent[0]) + + v, _ := i.s.Bool(MainsPresentKey) + assert.True(t, v) + + vp, _ := i.s.Bool(BatteryPresent(0)) + assert.True(t, vp) + }) + + t.Run("performed power configuration enumeration, if cluster present", func(t *testing.T) { + mzi := &implcaps.MockZDAInterface{} + defer mzi.AssertExpectations(t) + + mzi.On("Logger").Return(logwrap.New(discard.Discard())) + + ieeeAddress := zigbee.GenerateLocalAdministeredIEEEAddress() + mzi.On("TransmissionLookup", mock.Anything, zigbee.ProfileHomeAutomation).Return(ieeeAddress, zigbee.Endpoint(2), true, 4) + + mzc := &mocks.MockZCLCommunicator{} + defer mzc.AssertExpectations(t) + + mzi.On("ZCLCommunicator").Return(mzc) + + mam := &attribute.MockMonitor{} + defer mam.AssertExpectations(t) + + i := NewPowerSupply(mzi) + i.s = memory.New() + + i.mainsVoltageMonitor = mam + i.mainsFrequencyMonitor = mam + i.batteryVoltageMonitor[0] = mam + i.batteryPercentageMonitor[0] = mam + + mzc.On("ReadAttributes", mock.Anything, ieeeAddress, true, zcl.PowerConfigurationId, zigbee.NoManufacturer, zigbee.Endpoint(2), zigbee.Endpoint(1), uint8(4), []zcl.AttributeID{ + power_configuration.MainsVoltage, + power_configuration.MainsFrequency, + }).Return([]global.ReadAttributeResponseRecord{{Identifier: power_configuration.MainsVoltage, Status: 0}, {Identifier: power_configuration.MainsFrequency, Status: 0}}, nil) + + mam.On("Attach", mock.Anything, zigbee.Endpoint(1), zcl.PowerConfigurationId, power_configuration.MainsVoltage, zcl.TypeUnsignedInt16, mock.Anything, mock.Anything).Return(nil) + mam.On("Attach", mock.Anything, zigbee.Endpoint(1), zcl.PowerConfigurationId, power_configuration.MainsFrequency, zcl.TypeUnsignedInt8, mock.Anything, mock.Anything).Return(nil) + + mzc.On("ReadAttributes", mock.Anything, ieeeAddress, true, zcl.PowerConfigurationId, zigbee.NoManufacturer, zigbee.Endpoint(2), zigbee.Endpoint(1), uint8(4), []zcl.AttributeID{ + power_configuration.BatteryVoltage, + power_configuration.BatteryPercentageRemaining, + }).Return([]global.ReadAttributeResponseRecord{{Identifier: power_configuration.BatteryVoltage, Status: 0}, {Identifier: power_configuration.BatteryPercentageRemaining, Status: 0}}, nil) + + mam.On("Attach", mock.Anything, zigbee.Endpoint(1), zcl.PowerConfigurationId, power_configuration.BatteryVoltage, zcl.TypeUnsignedInt8, mock.Anything, mock.Anything).Return(nil) + mam.On("Attach", mock.Anything, zigbee.Endpoint(1), zcl.PowerConfigurationId, power_configuration.BatteryPercentageRemaining, zcl.TypeUnsignedInt8, mock.Anything, mock.Anything).Return(nil) + + mzc.On("ReadAttributes", mock.Anything, ieeeAddress, true, zcl.PowerConfigurationId, zigbee.NoManufacturer, zigbee.Endpoint(2), zigbee.Endpoint(1), uint8(4), []zcl.AttributeID{ + power_configuration.BatterySource2Voltage, + power_configuration.BatterySource2PercentageRemaining, + }).Return([]global.ReadAttributeResponseRecord{{Identifier: power_configuration.BatterySource2Voltage, Status: 1}, {Identifier: power_configuration.BatterySource2PercentageRemaining, Status: 1}}, nil) + + mzc.On("ReadAttributes", mock.Anything, ieeeAddress, true, zcl.PowerConfigurationId, zigbee.NoManufacturer, zigbee.Endpoint(2), zigbee.Endpoint(1), uint8(4), []zcl.AttributeID{ + power_configuration.BatterySource3Voltage, + power_configuration.BatterySource3PercentageRemaining, + }).Return([]global.ReadAttributeResponseRecord{{Identifier: power_configuration.BatterySource3Voltage, Status: 1}, {Identifier: power_configuration.BatterySource3PercentageRemaining, Status: 1}}, nil) + + attached, err := i.Enumerate(context.Background(), map[string]any{"ZigbeeEndpoint": zigbee.Endpoint(1), "ZigbeePowerConfigurationClusterPresent": true}) + assert.NoError(t, err) + assert.True(t, attached) + + assert.True(t, i.mainsPresent) + assert.True(t, i.mainsVoltagePresent) + assert.True(t, i.mainsFrequencyPresent) + assert.True(t, i.batteryPresent[0]) + assert.True(t, i.batteryVoltagePresent[0]) + assert.True(t, i.batteryPercentagePresent[0]) + + v, _ := i.s.Bool(MainsPresentKey) + assert.True(t, v) + v, _ = i.s.Bool(MainsVoltagePresentKey) + assert.True(t, v) + v, _ = i.s.Bool(MainsFrequencyPresentKey) + assert.True(t, v) + v, _ = i.s.Bool(BatteryPresent(0)) + assert.True(t, v) + v, _ = i.s.Bool(BatteryVoltagePresent(0)) + assert.True(t, v) + v, _ = i.s.Bool(BatteryPercentagePresent(0)) + assert.True(t, v) + }) +} + +func TestImplementation_Detach(t *testing.T) { + t.Run("detached attribute monitors on detach", func(t *testing.T) { + mm := &attribute.MockMonitor{} + defer mm.AssertExpectations(t) + + mm.On("Detach", mock.Anything, true).Return(nil).Times(8) + + mzi := &implcaps.MockZDAInterface{} + defer mzi.AssertExpectations(t) + + mzi.On("Logger").Return(logwrap.New(discard.Discard())) + + i := NewPowerSupply(mzi) + i.s = memory.New() + + i.mainsVoltagePresent = true + i.mainsFrequencyPresent = true + i.batteryPercentagePresent[0] = true + i.batteryVoltagePresent[0] = true + i.batteryPercentagePresent[1] = true + i.batteryVoltagePresent[1] = true + i.batteryPercentagePresent[2] = true + i.batteryVoltagePresent[2] = true + + i.mainsVoltageMonitor = mm + i.mainsFrequencyMonitor = mm + i.batteryPercentageMonitor[0] = mm + i.batteryVoltageMonitor[0] = mm + i.batteryPercentageMonitor[1] = mm + i.batteryVoltageMonitor[1] = mm + i.batteryPercentageMonitor[2] = mm + i.batteryVoltageMonitor[2] = mm + + err := i.Detach(context.TODO(), implcaps.NoLongerEnumerated) + assert.NoError(t, err) + }) +} + +func TestImplementation_update(t *testing.T) { + t.Run("mains voltage updates the state correctly, sending event if change", func(t *testing.T) { + mzi := &implcaps.MockZDAInterface{} + defer mzi.AssertExpectations(t) + + mzi.On("SendEvent", mock.Anything).Run(func(args mock.Arguments) { + e, ok := args.Get(0).(capabilities.PowerStatusUpdate) + assert.True(t, ok) + assert.InEpsilon(t, 239.5, e.PowerStatus.Mains[0].Voltage, 0.001) + }) + + mzi.On("Logger").Return(logwrap.New(discard.Discard())) + + i := NewPowerSupply(mzi) + i.s = memory.New() + + i.mainsPresent = true + i.mainsVoltagePresent = true + + i.s.Set(MainsVoltageKey, 239.0) + + lastUpdated := time.Now().Add(-5 * time.Minute) + converter.Store(i.s, implcaps.LastUpdatedKey, lastUpdated, converter.TimeEncoder) + converter.Store(i.s, implcaps.LastChangedKey, lastUpdated, converter.TimeEncoder) + + i.update(power_configuration.MainsVoltage, zcl.AttributeDataTypeValue{ + DataType: zcl.TypeUnsignedInt16, + Value: uint64(2395), + }) + + state, _ := i.Status(context.TODO()) + assert.InEpsilon(t, 239.5, state.Mains[0].Voltage, 0.001) + + lut, _ := i.LastUpdateTime(context.TODO()) + assert.Greater(t, lut, lastUpdated) + + lct, _ := i.LastChangeTime(context.TODO()) + assert.Greater(t, lct, lastUpdated) + }) + + t.Run("mains frequency updates the state correctly, sending event if change", func(t *testing.T) { + mzi := &implcaps.MockZDAInterface{} + defer mzi.AssertExpectations(t) + + mzi.On("SendEvent", mock.Anything).Run(func(args mock.Arguments) { + e, ok := args.Get(0).(capabilities.PowerStatusUpdate) + assert.True(t, ok) + assert.InEpsilon(t, 50.0, e.PowerStatus.Mains[0].Frequency, 0.001) + }) + + mzi.On("Logger").Return(logwrap.New(discard.Discard())) + + i := NewPowerSupply(mzi) + i.s = memory.New() + + i.mainsPresent = true + i.mainsFrequencyPresent = true + + i.s.Set(MainsFrequencyKey, 48) + + lastUpdated := time.Now().Add(-5 * time.Minute) + converter.Store(i.s, implcaps.LastUpdatedKey, lastUpdated, converter.TimeEncoder) + converter.Store(i.s, implcaps.LastChangedKey, lastUpdated, converter.TimeEncoder) + + i.update(power_configuration.MainsFrequency, zcl.AttributeDataTypeValue{ + DataType: zcl.TypeUnsignedInt8, + Value: uint64(100), + }) + + state, _ := i.Status(context.TODO()) + assert.InEpsilon(t, 50, state.Mains[0].Frequency, 0.001) + + lut, _ := i.LastUpdateTime(context.TODO()) + assert.Greater(t, lut, lastUpdated) + + lct, _ := i.LastChangeTime(context.TODO()) + assert.Greater(t, lct, lastUpdated) + }) + + t.Run("battery voltage updates the state correctly, sending event if change", func(t *testing.T) { + mzi := &implcaps.MockZDAInterface{} + defer mzi.AssertExpectations(t) + + mzi.On("SendEvent", mock.Anything).Run(func(args mock.Arguments) { + e, ok := args.Get(0).(capabilities.PowerStatusUpdate) + assert.True(t, ok) + assert.InEpsilon(t, 3.0, e.PowerStatus.Battery[0].Voltage, 0.001) + }) + + mzi.On("Logger").Return(logwrap.New(discard.Discard())) + + i := NewPowerSupply(mzi) + i.s = memory.New() + + i.batteryPresent[0] = true + i.batteryVoltagePresent[0] = true + + i.s.Set(BatteryVoltage(0), 3.1) + + lastUpdated := time.Now().Add(-5 * time.Minute) + converter.Store(i.s, implcaps.LastUpdatedKey, lastUpdated, converter.TimeEncoder) + converter.Store(i.s, implcaps.LastChangedKey, lastUpdated, converter.TimeEncoder) + + i.update(power_configuration.BatteryVoltage, zcl.AttributeDataTypeValue{ + DataType: zcl.TypeUnsignedInt8, + Value: uint64(30), + }) + + state, _ := i.Status(context.TODO()) + assert.InEpsilon(t, 3.0, state.Battery[0].Voltage, 0.001) + + lut, _ := i.LastUpdateTime(context.TODO()) + assert.Greater(t, lut, lastUpdated) + + lct, _ := i.LastChangeTime(context.TODO()) + assert.Greater(t, lct, lastUpdated) + }) + + t.Run("battery percent updates the state correctly, sending event if change", func(t *testing.T) { + mzi := &implcaps.MockZDAInterface{} + defer mzi.AssertExpectations(t) + + mzi.On("SendEvent", mock.Anything).Run(func(args mock.Arguments) { + e, ok := args.Get(0).(capabilities.PowerStatusUpdate) + assert.True(t, ok) + assert.InEpsilon(t, 0.995, e.PowerStatus.Battery[0].Remaining, 0.001) + }) + + mzi.On("Logger").Return(logwrap.New(discard.Discard())) + + i := NewPowerSupply(mzi) + i.s = memory.New() + + i.batteryPresent[0] = true + i.batteryPercentagePresent[0] = true + + i.s.Set(BatteryPercentage(1), 100) + + lastUpdated := time.Now().Add(-5 * time.Minute) + converter.Store(i.s, implcaps.LastUpdatedKey, lastUpdated, converter.TimeEncoder) + converter.Store(i.s, implcaps.LastChangedKey, lastUpdated, converter.TimeEncoder) + + i.update(power_configuration.BatteryPercentageRemaining, zcl.AttributeDataTypeValue{ + DataType: zcl.TypeUnsignedInt8, + Value: uint64(199), + }) + + state, _ := i.Status(context.TODO()) + assert.InEpsilon(t, 0.995, state.Battery[0].Remaining, 0.001) + + lut, _ := i.LastUpdateTime(context.TODO()) + assert.Greater(t, lut, lastUpdated) + + lct, _ := i.LastChangeTime(context.TODO()) + assert.Greater(t, lct, lastUpdated) + }) + + t.Run("updates the state correctly, no event", func(t *testing.T) { + mzi := &implcaps.MockZDAInterface{} + defer mzi.AssertExpectations(t) + + mzi.On("Logger").Return(logwrap.New(discard.Discard())) + + i := NewPowerSupply(mzi) + i.s = memory.New() + + i.mainsPresent = true + i.mainsVoltagePresent = true + + i.s.Set(MainsVoltageKey, 239.0) + + lastUpdated := time.UnixMilli(time.Now().Add(-5 * time.Minute).UnixMilli()) + converter.Store(i.s, implcaps.LastUpdatedKey, lastUpdated, converter.TimeEncoder) + converter.Store(i.s, implcaps.LastChangedKey, lastUpdated, converter.TimeEncoder) + + i.update(power_configuration.MainsVoltage, zcl.AttributeDataTypeValue{ + DataType: zcl.TypeUnsignedInt16, + Value: uint64(2390), + }) + + state, _ := i.Status(context.TODO()) + assert.InEpsilon(t, 239.0, state.Mains[0].Voltage, 0.001) + + lut, _ := i.LastUpdateTime(context.TODO()) + assert.Greater(t, lut, lastUpdated) + + lct, _ := i.LastChangeTime(context.TODO()) + assert.Equal(t, lct, lastUpdated) + }) +} + +func TestImplementation_Reading(t *testing.T) { + t.Run("returns the current power status", func(t *testing.T) { + mzi := &implcaps.MockZDAInterface{} + defer mzi.AssertExpectations(t) + + mzi.On("Logger").Return(logwrap.New(discard.Discard())) + + i := NewPowerSupply(mzi) + i.s = memory.New() + + i.mainsPresent = true + i.mainsVoltagePresent = true + i.mainsFrequencyPresent = true + i.batteryPresent[0] = true + i.batteryPresent[1] = true + i.batteryPresent[2] = true + i.batteryPercentagePresent[0] = true + i.batteryPercentagePresent[1] = true + i.batteryPercentagePresent[2] = true + i.batteryVoltagePresent[0] = true + i.batteryVoltagePresent[1] = true + i.batteryVoltagePresent[2] = true + + i.s.Set(MainsVoltageKey, 230.0) + i.s.Set(MainsFrequencyKey, 50.0) + i.s.Set(BatteryPercentage(0), 100.0) + i.s.Set(BatteryPercentage(1), 75.0) + i.s.Set(BatteryPercentage(2), 50.0) + i.s.Set(BatteryVoltage(0), 3.0) + i.s.Set(BatteryVoltage(1), 2.9) + i.s.Set(BatteryVoltage(2), 2.8) + + status, err := i.Status(context.TODO()) + assert.NoError(t, err) + + assert.Len(t, status.Mains, 1) + assert.Len(t, status.Battery, 3) + + assert.Equal(t, capabilities.Voltage|capabilities.Frequency, status.Mains[0].Present) + assert.Equal(t, capabilities.Voltage|capabilities.Remaining, status.Battery[0].Present) + assert.Equal(t, capabilities.Voltage|capabilities.Remaining, status.Battery[1].Present) + assert.Equal(t, capabilities.Voltage|capabilities.Remaining, status.Battery[2].Present) + + assert.InDelta(t, 230.0, status.Mains[0].Voltage, 0.0001) + assert.InDelta(t, 50.0, status.Mains[0].Frequency, 0.0001) + + assert.InDelta(t, 3.0, status.Battery[0].Voltage, 0.0001) + assert.InDelta(t, 1.0, status.Battery[0].Remaining, 0.0001) + + assert.InDelta(t, 2.9, status.Battery[1].Voltage, 0.0001) + assert.InDelta(t, 0.75, status.Battery[1].Remaining, 0.0001) + + assert.InDelta(t, 2.8, status.Battery[2].Voltage, 0.0001) + assert.InDelta(t, 0.5, status.Battery[2].Remaining, 0.0001) + }) +} + +func TestImplementation_LastTimes(t *testing.T) { + t.Run("returns the last updated and changed times", func(t *testing.T) { + mzi := &implcaps.MockZDAInterface{} + defer mzi.AssertExpectations(t) + + mzi.On("Logger").Return(logwrap.New(discard.Discard())) + + i := NewPowerSupply(mzi) + i.s = memory.New() + + changedTime := time.UnixMilli(time.Now().UnixMilli()) + updatedTime := changedTime.Add(5 * time.Minute) + + converter.Store(i.s, implcaps.LastUpdatedKey, updatedTime, converter.TimeEncoder) + converter.Store(i.s, implcaps.LastChangedKey, changedTime, converter.TimeEncoder) + + lct, err := i.LastChangeTime(context.TODO()) + assert.NoError(t, err) + assert.Equal(t, changedTime, lct) + + lut, err := i.LastUpdateTime(context.TODO()) + assert.NoError(t, err) + assert.Equal(t, updatedTime, lut) + }) +} diff --git a/implcaps/zcl/pressure_sensor/impl.go b/implcaps/zcl/pressure_sensor/impl.go index 045e55c..6f91293 100644 --- a/implcaps/zcl/pressure_sensor/impl.go +++ b/implcaps/zcl/pressure_sensor/impl.go @@ -57,8 +57,6 @@ func (i *Implementation) Load(ctx context.Context) (bool, error) { func (i *Implementation) Enumerate(ctx context.Context, m map[string]any) (bool, error) { endpoint := implcaps.Get(m, "ZigbeeEndpoint", zigbee.Endpoint(1)) - clusterId := implcaps.Get(m, "ZigbeePressureMeasurementClusterID", zcl.PressureMeasurementId) - attributeId := implcaps.Get(m, "ZigbeePressureMeasurementAttributeID", pressure_measurement.MeasuredValue) reporting := attribute.ReportingConfig{ Mode: attribute.AttemptConfigureReporting, @@ -72,7 +70,7 @@ func (i *Implementation) Enumerate(ctx context.Context, m map[string]any) (bool, Interval: 1 * time.Minute, } - if err := i.am.Attach(ctx, endpoint, clusterId, attributeId, zcl.TypeSignedInt16, reporting, polling); err != nil { + if err := i.am.Attach(ctx, endpoint, zcl.PressureMeasurementId, pressure_measurement.MeasuredValue, zcl.TypeSignedInt16, reporting, polling); err != nil { return false, err } diff --git a/implcaps/zcl/pressure_sensor/impl_test.go b/implcaps/zcl/pressure_sensor/impl_test.go index 4b9a4fd..09e3ca1 100644 --- a/implcaps/zcl/pressure_sensor/impl_test.go +++ b/implcaps/zcl/pressure_sensor/impl_test.go @@ -82,7 +82,7 @@ func TestImplementation_Load(t *testing.T) { } func TestImplementation_Enumerate(t *testing.T) { - t.Run("attaches to the attribute monitor, using default attributes", func(t *testing.T) { + t.Run("attaches to the attribute monitor", func(t *testing.T) { mm := &attribute.MockMonitor{} defer mm.AssertExpectations(t) @@ -96,26 +96,6 @@ func TestImplementation_Enumerate(t *testing.T) { assert.NoError(t, err) }) - t.Run("attaches to the attribute monitor, using overridden attributes", func(t *testing.T) { - mm := &attribute.MockMonitor{} - defer mm.AssertExpectations(t) - - mm.On("Attach", mock.Anything, zigbee.Endpoint(0x02), zigbee.ClusterID(0x500), zcl.AttributeID(0x10), zcl.TypeSignedInt16, mock.Anything, mock.Anything).Return(nil) - - i := NewPressureSensor(nil) - i.am = mm - - attributes := map[string]any{ - "ZigbeeEndpoint": zigbee.Endpoint(0x02), - "ZigbeePressureMeasurementClusterID": zigbee.ClusterID(0x500), - "ZigbeePressureMeasurementAttributeID": zcl.AttributeID(0x10), - } - attached, err := i.Enumerate(context.TODO(), attributes) - - assert.True(t, attached) - assert.NoError(t, err) - }) - t.Run("fails if attach to the attribute monitor fails", func(t *testing.T) { mm := &attribute.MockMonitor{} defer mm.AssertExpectations(t) diff --git a/implcaps/zcl/temperature_sensor/impl.go b/implcaps/zcl/temperature_sensor/impl.go index 2d66459..b656314 100644 --- a/implcaps/zcl/temperature_sensor/impl.go +++ b/implcaps/zcl/temperature_sensor/impl.go @@ -57,8 +57,6 @@ func (i *Implementation) Load(ctx context.Context) (bool, error) { func (i *Implementation) Enumerate(ctx context.Context, m map[string]any) (bool, error) { endpoint := implcaps.Get(m, "ZigbeeEndpoint", zigbee.Endpoint(1)) - clusterId := implcaps.Get(m, "ZigbeeTemperatureMeasurementClusterID", zcl.TemperatureMeasurementId) - attributeId := implcaps.Get(m, "ZigbeeTemperatureMeasurementAttributeID", temperature_measurement.MeasuredValue) reporting := attribute.ReportingConfig{ Mode: attribute.AttemptConfigureReporting, @@ -72,7 +70,7 @@ func (i *Implementation) Enumerate(ctx context.Context, m map[string]any) (bool, Interval: 1 * time.Minute, } - if err := i.am.Attach(ctx, endpoint, clusterId, attributeId, zcl.TypeSignedInt16, reporting, polling); err != nil { + if err := i.am.Attach(ctx, endpoint, zcl.TemperatureMeasurementId, temperature_measurement.MeasuredValue, zcl.TypeSignedInt16, reporting, polling); err != nil { return false, err } diff --git a/implcaps/zcl/temperature_sensor/impl_test.go b/implcaps/zcl/temperature_sensor/impl_test.go index 977b83b..ef5053b 100644 --- a/implcaps/zcl/temperature_sensor/impl_test.go +++ b/implcaps/zcl/temperature_sensor/impl_test.go @@ -82,7 +82,7 @@ func TestImplementation_Load(t *testing.T) { } func TestImplementation_Enumerate(t *testing.T) { - t.Run("attaches to the attribute monitor, using default attributes", func(t *testing.T) { + t.Run("attaches to the attribute monitor", func(t *testing.T) { mm := &attribute.MockMonitor{} defer mm.AssertExpectations(t) @@ -96,26 +96,6 @@ func TestImplementation_Enumerate(t *testing.T) { assert.NoError(t, err) }) - t.Run("attaches to the attribute monitor, using overridden attributes", func(t *testing.T) { - mm := &attribute.MockMonitor{} - defer mm.AssertExpectations(t) - - mm.On("Attach", mock.Anything, zigbee.Endpoint(0x02), zigbee.ClusterID(0x500), zcl.AttributeID(0x10), zcl.TypeSignedInt16, mock.Anything, mock.Anything).Return(nil) - - i := NewTemperatureSensor(nil) - i.am = mm - - attributes := map[string]any{ - "ZigbeeEndpoint": zigbee.Endpoint(0x02), - "ZigbeeTemperatureMeasurementClusterID": zigbee.ClusterID(0x500), - "ZigbeeTemperatureMeasurementAttributeID": zcl.AttributeID(0x10), - } - attached, err := i.Enumerate(context.TODO(), attributes) - - assert.True(t, attached) - assert.NoError(t, err) - }) - t.Run("fails if attach to the attribute monitor fails", func(t *testing.T) { mm := &attribute.MockMonitor{} defer mm.AssertExpectations(t) diff --git a/node.go b/node.go index 3514b7d..7ff19fb 100644 --- a/node.go +++ b/node.go @@ -85,7 +85,7 @@ type node struct { func makeTransactionSequence() chan uint8 { ch := make(chan uint8, math.MaxUint8) - for i := uint8(0); i < math.MaxUint8; i++ { + for i := range uint8(math.MaxUint8) { ch <- i } diff --git a/rules/zcl.json b/rules/zcl.json index 5b992d8..852bad0 100644 --- a/rules/zcl.json +++ b/rules/zcl.json @@ -17,11 +17,13 @@ } }, { - "Filter": "(0x0001 in Endpoint[Self].InClusters)", + "Filter": "(0x0000 in Endpoint[Self].InClusters || 0x0001 in Endpoint[Self].InClusters)", "Actions": { "Capabilities": { "Add": { "ZCLPowerSupply": { + "ZigbeeBasicClusterPresent": "(0x0000 in Endpoint[Self].InClusters)", + "ZigbeePowerConfigurationClusterPresent": "(0x0001 in Endpoint[Self].InClusters)", "ZigbeeEndpoint": "Fn.Endpoint(Self)" } } diff --git a/zda_interface.go b/zda_interface.go index 0d59e39..89f4418 100644 --- a/zda_interface.go +++ b/zda_interface.go @@ -2,6 +2,7 @@ package zda import ( "github.com/shimmeringbee/da" + "github.com/shimmeringbee/logwrap" "github.com/shimmeringbee/zcl" "github.com/shimmeringbee/zcl/communicator" "github.com/shimmeringbee/zda/attribute" @@ -16,6 +17,10 @@ type zdaInterface struct { c communicator.Communicator } +func (z zdaInterface) Logger() logwrap.Logger { + return z.gw.logger +} + func (z zdaInterface) ZCLRegister(f func(*zcl.CommandRegistry)) { f(z.gw.zclCommandRegistry) }