From 5e9da94b2df8c97224a95503fac46d40c89dabcf Mon Sep 17 00:00:00 2001 From: Peter Wood Date: Sun, 23 Jun 2024 20:43:50 +0100 Subject: [PATCH] Rework device workarounds. --- device_test.go | 4 +- enumerate_device_test.go | 10 +- go.mod | 2 +- go.sum | 31 +--- implcaps/factory/mapping.go | 24 +-- implcaps/generic/device_workarounds/impl.go | 139 ++++++++++++++++++ .../generic/device_workarounds/impl_test.go | 63 ++++++++ .../impl.go} | 42 +++--- .../impl_test.go} | 14 +- implcaps/interface.go | 2 + implcaps/mock.go | 4 + .../tirouter/device_workaround/impl.go | 88 ----------- .../tirouter/device_workaround/impl_test.go | 125 ---------------- rules/engine.go | 5 +- rules/generic.json | 34 +++++ rules/proprietary.json | 18 --- rules/xaiomi.json | 55 ------- rules/zcl.json | 18 +-- rules/zstack.json | 19 --- table_test.go | 8 +- zda_interface.go | 4 + 21 files changed, 316 insertions(+), 393 deletions(-) create mode 100644 implcaps/generic/device_workarounds/impl.go create mode 100644 implcaps/generic/device_workarounds/impl_test.go rename implcaps/generic/{product_information.go => product_information/impl.go} (57%) rename implcaps/generic/{product_information_test.go => product_information/impl_test.go} (89%) delete mode 100644 implcaps/proprietary/tirouter/device_workaround/impl.go delete mode 100644 implcaps/proprietary/tirouter/device_workaround/impl_test.go create mode 100644 rules/generic.json delete mode 100644 rules/proprietary.json delete mode 100644 rules/xaiomi.json delete mode 100644 rules/zstack.json diff --git a/device_test.go b/device_test.go index 5223047..db3cbe4 100644 --- a/device_test.go +++ b/device_test.go @@ -5,7 +5,7 @@ import ( "github.com/shimmeringbee/da/capabilities" "github.com/shimmeringbee/da/mocks" "github.com/shimmeringbee/zda/implcaps" - "github.com/shimmeringbee/zda/implcaps/generic" + "github.com/shimmeringbee/zda/implcaps/generic/product_information" "github.com/shimmeringbee/zigbee" "github.com/stretchr/testify/assert" "sync" @@ -42,7 +42,7 @@ func Test_device(t *testing.T) { }) t.Run("Device returns the stored capability", func(t *testing.T) { - c := &generic.ProductInformation{} + c := &product_information.Implementation{} d := device{ capabilities: map[da.Capability]implcaps.ZDACapability{capabilities.ProductInformationFlag: c}, diff --git a/enumerate_device_test.go b/enumerate_device_test.go index 1f9fa89..9bd213d 100644 --- a/enumerate_device_test.go +++ b/enumerate_device_test.go @@ -12,7 +12,7 @@ import ( "github.com/shimmeringbee/zcl/commands/local/basic" "github.com/shimmeringbee/zda/implcaps" "github.com/shimmeringbee/zda/implcaps/factory" - "github.com/shimmeringbee/zda/implcaps/generic" + "github.com/shimmeringbee/zda/implcaps/generic/product_information" "github.com/shimmeringbee/zda/rules" "github.com/shimmeringbee/zigbee" "github.com/stretchr/testify/assert" @@ -496,7 +496,7 @@ func Test_enumerateDevice_updateCapabilitiesOnDevice(t *testing.T) { } mdm.On("attachCapabilityToDevice", d, mock.Anything).Run(func(args mock.Arguments) { - pic := args.Get(1).(*generic.ProductInformation) + pic := args.Get(1).(*product_information.Implementation) pi, _ := pic.Get(context.Background()) assert.Equal(t, "NEXUS-7", pi.Name) }) @@ -513,7 +513,7 @@ func Test_enumerateDevice_updateCapabilitiesOnDevice(t *testing.T) { mdm := &mockDeviceManager{} defer mdm.AssertExpectations(t) ed := enumerateDevice{logger: logwrap.New(discard.Discard()), capabilityFactory: factory.Create, dm: mdm} - opi := generic.NewProductInformation() + opi := product_information.NewProductInformation() d := &device{m: &sync.RWMutex{}, deviceId: 1, capabilities: map[da.Capability]implcaps.ZDACapability{capabilities.ProductInformationFlag: opi}} opi.Init(d, memory.New()) _, _ = opi.Enumerate(context.Background(), map[string]any{ @@ -536,7 +536,7 @@ func Test_enumerateDevice_updateCapabilitiesOnDevice(t *testing.T) { } mdm.On("attachCapabilityToDevice", d, mock.Anything).Run(func(args mock.Arguments) { - pic := args.Get(1).(*generic.ProductInformation) + pic := args.Get(1).(*product_information.Implementation) pi, _ := pic.Get(context.Background()) assert.Equal(t, "NEXUS-7", pi.Name) }) @@ -553,7 +553,7 @@ func Test_enumerateDevice_updateCapabilitiesOnDevice(t *testing.T) { mdm := &mockDeviceManager{} defer mdm.AssertExpectations(t) ed := enumerateDevice{logger: logwrap.New(discard.Discard()), capabilityFactory: factory.Create, dm: mdm} - opi := generic.NewProductInformation() + opi := product_information.NewProductInformation() d := &device{m: &sync.RWMutex{}, deviceId: 1, capabilities: map[da.Capability]implcaps.ZDACapability{capabilities.ProductInformationFlag: opi}} opi.Init(d, memory.New()) _, _ = opi.Enumerate(context.Background(), map[string]any{ diff --git a/go.mod b/go.mod index 16e6c7c..68829d0 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,7 @@ go 1.22.0 require ( github.com/expr-lang/expr v1.16.9 github.com/shimmeringbee/callbacks v0.0.0-20240614104656-b56cd6b4b604 - github.com/shimmeringbee/da v0.0.0-20240621202518-c4ba1b61593d + github.com/shimmeringbee/da v0.0.0-20240622202110-ccbf24700388 github.com/shimmeringbee/logwrap v0.1.3 github.com/shimmeringbee/persistence v0.0.0-20240615183316-1a60e6781413 github.com/shimmeringbee/retry v0.0.0-20240614104711-064c2726a8b4 diff --git a/go.sum b/go.sum index 3659006..0d97b90 100644 --- a/go.sum +++ b/go.sum @@ -9,48 +9,21 @@ github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69 github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/shimmeringbee/bytecodec v0.0.0-20210228205504-1e9e0677347b h1:8q7X6JQwYKjnl+Absfv9m+LbDSBBllqTDDKzmtZ1ybY= github.com/shimmeringbee/bytecodec v0.0.0-20210228205504-1e9e0677347b/go.mod h1:WYnxfxTJ45UQ+xeAuuTSIalcEepgP8Rb7T/OhCaDdgo= github.com/shimmeringbee/bytecodec v0.0.0-20240614104652-9d31c74dcd13 h1:GiNQq9XzoQerCE/eI8/NEPJ7W+iy0FRSn6whgnPlb3w= github.com/shimmeringbee/bytecodec v0.0.0-20240614104652-9d31c74dcd13/go.mod h1:WYnxfxTJ45UQ+xeAuuTSIalcEepgP8Rb7T/OhCaDdgo= -github.com/shimmeringbee/callbacks v0.0.0-20221001135028-b85b5f89d5d6 h1:A1t2A3OrXEEdZqMw+zmwe6r7LbeZWP256es5TY9/k3Y= -github.com/shimmeringbee/callbacks v0.0.0-20221001135028-b85b5f89d5d6/go.mod h1:1AzT3lP4dAEaqWDdWsldhRtcl0+jyCGcZaBTHTjtA9w= github.com/shimmeringbee/callbacks v0.0.0-20240614104656-b56cd6b4b604 h1:he/14/56+C/b7y57sHfU/IqyB4gSyexfHkMuq3egcJg= github.com/shimmeringbee/callbacks v0.0.0-20240614104656-b56cd6b4b604/go.mod h1:1AzT3lP4dAEaqWDdWsldhRtcl0+jyCGcZaBTHTjtA9w= -github.com/shimmeringbee/da v0.0.0-20240510193548-c78db0273744 h1:71a3cbHePCjYYH5ki+RqMVteeHiFKVP7VlwFi451fys= -github.com/shimmeringbee/da v0.0.0-20240510193548-c78db0273744/go.mod h1:jUKTa353LvJT3TAdwtmfGEbcxkYbG58h0gbASRf0FIs= -github.com/shimmeringbee/da v0.0.0-20240614104703-d8efc964b76a h1:gqBGoPcDjCzttr8n1nVFORHv3YFOPCNrfcFbu6P4X8A= -github.com/shimmeringbee/da v0.0.0-20240614104703-d8efc964b76a/go.mod h1:jUKTa353LvJT3TAdwtmfGEbcxkYbG58h0gbASRf0FIs= -github.com/shimmeringbee/da v0.0.0-20240615193906-993b4195d59e h1:Abre4czD9AJseZEL5CZplaZBQtr8Ztglj6elZofylyY= -github.com/shimmeringbee/da v0.0.0-20240615193906-993b4195d59e/go.mod h1:jUKTa353LvJT3TAdwtmfGEbcxkYbG58h0gbASRf0FIs= -github.com/shimmeringbee/da v0.0.0-20240615210808-95a0a3f8f08a h1:e6YUEZ4ljwrNG2So0QJnRvxLQVTKh5uHP9MuzNV1/Vc= -github.com/shimmeringbee/da v0.0.0-20240615210808-95a0a3f8f08a/go.mod h1:jUKTa353LvJT3TAdwtmfGEbcxkYbG58h0gbASRf0FIs= -github.com/shimmeringbee/da v0.0.0-20240620203531-59614abd3e28 h1:rXtMUoERVyQ+lzXSiQbFy7HRNMu9sE3rvRupLTH1zbA= -github.com/shimmeringbee/da v0.0.0-20240620203531-59614abd3e28/go.mod h1:jUKTa353LvJT3TAdwtmfGEbcxkYbG58h0gbASRf0FIs= -github.com/shimmeringbee/da v0.0.0-20240621202518-c4ba1b61593d h1:b0pgl4liNcXKi07Y3nVuzZ1uLKPJdkFA6/Y7hsUNSzM= -github.com/shimmeringbee/da v0.0.0-20240621202518-c4ba1b61593d/go.mod h1:jUKTa353LvJT3TAdwtmfGEbcxkYbG58h0gbASRf0FIs= +github.com/shimmeringbee/da v0.0.0-20240622202110-ccbf24700388 h1:opK7x4lWbylMm5PfCzixiG7s07FtinqNlbCat+Md7+c= +github.com/shimmeringbee/da v0.0.0-20240622202110-ccbf24700388/go.mod h1:jUKTa353LvJT3TAdwtmfGEbcxkYbG58h0gbASRf0FIs= github.com/shimmeringbee/logwrap v0.1.3 h1:1PqPGdgbeQxACQqc6RUWERn7EnpA1jbiHzXVYFa7q2A= github.com/shimmeringbee/logwrap v0.1.3/go.mod h1:NBAcZCUl6aFOGnWTs8m67EUAmWFZXRhoRQf5nknY8W0= -github.com/shimmeringbee/persistence v0.0.0-20240614163143-a99424e4d61c h1:c86y9DJNOsvZHZ/XAwfzdIxmvPPRPsWhqylF8Ok6xfg= -github.com/shimmeringbee/persistence v0.0.0-20240614163143-a99424e4d61c/go.mod h1:Ob1eKGYM7+9P3LkB9vB9nr15d3trtS4D9KOnGoxOkp8= -github.com/shimmeringbee/persistence v0.0.0-20240615120347-680c4a887f1b h1:TD++F59YAdQCD7xeEmUDlX2nPmyP5LgVoD6IiaVgD1c= -github.com/shimmeringbee/persistence v0.0.0-20240615120347-680c4a887f1b/go.mod h1:Ob1eKGYM7+9P3LkB9vB9nr15d3trtS4D9KOnGoxOkp8= -github.com/shimmeringbee/persistence v0.0.0-20240615120714-a567d1ac9349 h1:IQ2JhhUVsATqnGav6KOJRqLFDZ80wCYYG3yawTVlylI= -github.com/shimmeringbee/persistence v0.0.0-20240615120714-a567d1ac9349/go.mod h1:Ob1eKGYM7+9P3LkB9vB9nr15d3trtS4D9KOnGoxOkp8= -github.com/shimmeringbee/persistence v0.0.0-20240615141034-6414db99d48e h1:hxB9Zczd27kKbOMoiK56CaHgFKDyIYQLVCD1WV+LlgA= -github.com/shimmeringbee/persistence v0.0.0-20240615141034-6414db99d48e/go.mod h1:Ob1eKGYM7+9P3LkB9vB9nr15d3trtS4D9KOnGoxOkp8= github.com/shimmeringbee/persistence v0.0.0-20240615183316-1a60e6781413 h1:0t4xEtg+kXXATiryn0m9p5OmsvDUvh2BPG+r7Vcpvy4= github.com/shimmeringbee/persistence v0.0.0-20240615183316-1a60e6781413/go.mod h1:Ob1eKGYM7+9P3LkB9vB9nr15d3trtS4D9KOnGoxOkp8= -github.com/shimmeringbee/retry v0.0.0-20221006193055-2ce01bf139c2 h1:HxpPz7w7SxVf1GmcM5oTK1JK64TGpK1UflweYRSOwC4= -github.com/shimmeringbee/retry v0.0.0-20221006193055-2ce01bf139c2/go.mod h1:KYvVq5b7/BSSlWng+AKB5jwNGpc0D7eg8ySWrdPAlms= github.com/shimmeringbee/retry v0.0.0-20240614104711-064c2726a8b4 h1:YU77guV/6/9nJymm4K1JH6MIx6yE/NfUnFX//yo3GfM= github.com/shimmeringbee/retry v0.0.0-20240614104711-064c2726a8b4/go.mod h1:KYvVq5b7/BSSlWng+AKB5jwNGpc0D7eg8ySWrdPAlms= -github.com/shimmeringbee/zcl v0.0.0-20240614085730-dc48ec71c312 h1:OoFxM9W299ESRnPSR3NoK4+AmWz5PzcJ4gJFZ62nkZ0= -github.com/shimmeringbee/zcl v0.0.0-20240614085730-dc48ec71c312/go.mod h1:1WBb6RqMCVEukSmLY4fN/F5p3YYZrm6PTYn2TBnHwis= github.com/shimmeringbee/zcl v0.0.0-20240614104719-4eee02c0ffd1 h1:19JMz+jKs8poUPlmF769Z2e+zZjmACS+aLB2BHFTKHE= github.com/shimmeringbee/zcl v0.0.0-20240614104719-4eee02c0ffd1/go.mod h1:DeGINQ0C9S61qBON9Zm2RArEBX4ap1LyHClfUgSUTEM= -github.com/shimmeringbee/zigbee v0.0.0-20221016122511-6c2328db0d94/go.mod h1:GMA6rVpzvUK16cZwi8uW11JUTx8xUGOk5DbkXYWvm/8= -github.com/shimmeringbee/zigbee v0.0.0-20240614103911-3a30074e1528 h1:D5jQVQ/kMjiVp4bYYmuWdKvW81+1tv2arSTgiXKkWmM= github.com/shimmeringbee/zigbee v0.0.0-20240614103911-3a30074e1528/go.mod h1:BDCm9qtlJANPiLY+YRQac/0awPxeUd3FUxUFPh+1w/s= github.com/shimmeringbee/zigbee v0.0.0-20240614104723-f4c0c0231568 h1:DnZ/kbXJZtihjqB7mz92hhUeP0+v0jYl5DJIznWdlL4= github.com/shimmeringbee/zigbee v0.0.0-20240614104723-f4c0c0231568/go.mod h1:BDCm9qtlJANPiLY+YRQac/0awPxeUd3FUxUFPh+1w/s= diff --git a/implcaps/factory/mapping.go b/implcaps/factory/mapping.go index 8919661..b1aa668 100644 --- a/implcaps/factory/mapping.go +++ b/implcaps/factory/mapping.go @@ -4,8 +4,8 @@ import ( "github.com/shimmeringbee/da" "github.com/shimmeringbee/da/capabilities" "github.com/shimmeringbee/zda/implcaps" - "github.com/shimmeringbee/zda/implcaps/generic" - "github.com/shimmeringbee/zda/implcaps/proprietary/tirouter/device_workaround" + "github.com/shimmeringbee/zda/implcaps/generic/device_workarounds" + "github.com/shimmeringbee/zda/implcaps/generic/product_information" "github.com/shimmeringbee/zda/implcaps/zcl/humidity_sensor" "github.com/shimmeringbee/zda/implcaps/zcl/identify" "github.com/shimmeringbee/zda/implcaps/zcl/power_supply" @@ -19,22 +19,22 @@ const ZCLHumiditySensor = "ZCLHumiditySensor" const ZCLPressureSensor = "ZCLPressureSensor" const ZCLIdentify = "ZCLIdentify" const ZCLPowerSupply = "ZCLPowerSupply" -const ProprietaryTiRouterDeviceWorkaround = "ProprietaryTiRouterDeviceWorkaround" +const GenericDeviceWorkarounds = "GenericDeviceWorkarounds" var Mapping = map[string]da.Capability{ - GenericProductInformation: capabilities.ProductInformationFlag, - ZCLTemperatureSensor: capabilities.TemperatureSensorFlag, - ZCLHumiditySensor: capabilities.RelativeHumiditySensorFlag, - ZCLPressureSensor: capabilities.PressureSensorFlag, - ZCLIdentify: capabilities.IdentifyFlag, - ZCLPowerSupply: capabilities.PowerSupplyFlag, - ProprietaryTiRouterDeviceWorkaround: capabilities.DeviceWorkaroundFlag, + GenericProductInformation: capabilities.ProductInformationFlag, + ZCLTemperatureSensor: capabilities.TemperatureSensorFlag, + ZCLHumiditySensor: capabilities.RelativeHumiditySensorFlag, + ZCLPressureSensor: capabilities.PressureSensorFlag, + ZCLIdentify: capabilities.IdentifyFlag, + ZCLPowerSupply: capabilities.PowerSupplyFlag, + GenericDeviceWorkarounds: capabilities.DeviceWorkaroundsFlag, } func Create(name string, iface implcaps.ZDAInterface) implcaps.ZDACapability { switch name { case GenericProductInformation: - return generic.NewProductInformation() + return product_information.NewProductInformation() case ZCLTemperatureSensor: return temperature_sensor.NewTemperatureSensor(iface) case ZCLHumiditySensor: @@ -45,7 +45,7 @@ func Create(name string, iface implcaps.ZDAInterface) implcaps.ZDACapability { return identify.NewIdentify(iface) case ZCLPowerSupply: return power_suply.NewPowerSupply(iface) - case ProprietaryTiRouterDeviceWorkaround: + case GenericDeviceWorkarounds: return device_workaround.NewDeviceWorkaround(iface) default: return nil diff --git a/implcaps/generic/device_workarounds/impl.go b/implcaps/generic/device_workarounds/impl.go new file mode 100644 index 0000000..c686493 --- /dev/null +++ b/implcaps/generic/device_workarounds/impl.go @@ -0,0 +1,139 @@ +package device_workaround + +import ( + "context" + "github.com/shimmeringbee/da" + "github.com/shimmeringbee/da/capabilities" + "github.com/shimmeringbee/persistence" + "github.com/shimmeringbee/zcl" + "github.com/shimmeringbee/zcl/commands/local/basic" + "github.com/shimmeringbee/zda/implcaps" + "github.com/shimmeringbee/zigbee" + "strings" + "sync" +) + +var _ implcaps.ZDACapability = (*Implementation)(nil) +var _ capabilities.DeviceWorkarounds = (*Implementation)(nil) + +func NewDeviceWorkaround(zi implcaps.ZDAInterface) *Implementation { + return &Implementation{zi: zi, m: &sync.RWMutex{}} +} + +type Implementation struct { + s persistence.Section + d da.Device + zi implcaps.ZDAInterface + + m *sync.RWMutex + workaroundsEnabled []string +} + +func (i *Implementation) Capability() da.Capability { + return capabilities.DeviceWorkaroundsFlag +} + +func (i *Implementation) Name() string { + return capabilities.StandardNames[capabilities.DeviceWorkaroundsFlag] +} + +func (i *Implementation) Init(d da.Device, s persistence.Section) { + i.d = d + i.s = s +} + +func (i *Implementation) Load(ctx context.Context) (bool, error) { + i.m.Lock() + defer i.m.Unlock() + + i.workaroundsEnabled = i.s.Section("Workarounds").SectionKeys() + + for _, workaround := range i.workaroundsEnabled { + if err := i.loadWorkaround(ctx, workaround); err != nil { + return false, err + } + } + + return true, nil +} + +func (i *Implementation) Enumerate(ctx context.Context, m map[string]any) (bool, error) { + var workarounds []string + + for k, _ := range m { + if strings.HasPrefix(k, "Enable") { + workarounds = append(workarounds, k) + } + } + + i.m.Lock() + i.workaroundsEnabled = workarounds + i.m.Unlock() + + for _, workaround := range workarounds { + if err := i.enumerateWorkaround(ctx, m, workaround); err != nil { + return false, err + } + } + + return true, nil +} + +func (i *Implementation) Detach(ctx context.Context, detachType implcaps.DetachType) error { + i.m.Lock() + defer i.m.Unlock() + + for _, workaround := range i.workaroundsEnabled { + if err := i.detachWorkaround(ctx, detachType, workaround); err != nil { + return err + } + } + + return nil +} + +func (i *Implementation) ImplName() string { + return "GenericDeviceWorkarounds" +} + +func (i *Implementation) Enabled(_ context.Context) []string { + i.m.RLock() + defer i.m.RUnlock() + + return i.workaroundsEnabled +} + +func (i *Implementation) loadWorkaround(ctx context.Context, workaround string) error { + return nil +} + +func (i *Implementation) detachWorkaround(ctx context.Context, detachType implcaps.DetachType, workaround string) error { + return nil +} + +func (i *Implementation) enumerateWorkaround(ctx context.Context, m map[string]any, workaround string) error { + switch workaround { + case "EnableZCLReportingKeepAlive": + return i.enumerateZCLReportingKeepAlive(ctx, m) + } + + return nil +} + +func (i *Implementation) enumerateZCLReportingKeepAlive(ctx context.Context, m map[string]any) error { + remoteEndpoint := implcaps.Get(m, "ZigbeeEndpoint", zigbee.Endpoint(1)) + + ieeeAddress, localEndpoint, ack, seq := i.zi.TransmissionLookup(i.d, zigbee.ProfileHomeAutomation) + + if err := i.zi.NodeBinder().BindNodeToController(ctx, ieeeAddress, localEndpoint, remoteEndpoint, zcl.BasicId); err != nil { + return err + } + + if err := i.zi.ZCLCommunicator().ConfigureReporting(ctx, ieeeAddress, ack, zcl.BasicId, zigbee.NoManufacturer, localEndpoint, remoteEndpoint, seq, basic.ZCLVersion, zcl.TypeUnsignedInt8, uint16(60), uint16(240), 0); err != nil { + return err + } + + i.s.Section("Workarounds", "ZCLReportingKeepAlive") + + return nil +} diff --git a/implcaps/generic/device_workarounds/impl_test.go b/implcaps/generic/device_workarounds/impl_test.go new file mode 100644 index 0000000..890cdaa --- /dev/null +++ b/implcaps/generic/device_workarounds/impl_test.go @@ -0,0 +1,63 @@ +package device_workaround + +import ( + "github.com/shimmeringbee/da/capabilities" + "github.com/shimmeringbee/da/mocks" + "github.com/shimmeringbee/persistence/impl/memory" + "github.com/shimmeringbee/zcl" + "github.com/shimmeringbee/zcl/commands/local/basic" + "github.com/shimmeringbee/zda/implcaps" + mocks2 "github.com/shimmeringbee/zda/mocks" + "github.com/shimmeringbee/zigbee" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "testing" +) + +func TestImplementation_BaseFunctions(t *testing.T) { + t.Run("basic static functions respond correctly", func(t *testing.T) { + i := NewDeviceWorkaround(nil) + + assert.Equal(t, capabilities.DeviceWorkaroundsFlag, i.Capability()) + assert.Equal(t, capabilities.StandardNames[capabilities.DeviceWorkaroundsFlag], i.Name()) + assert.Equal(t, "GenericDeviceWorkarounds", i.ImplName()) + }) +} + +func TestImplementation_EnableZCLReportingKeepAlive(t *testing.T) { + t.Run("configures reporting for ZCLVersion on Enumeration", func(t *testing.T) { + mzi := &implcaps.MockZDAInterface{} + defer mzi.AssertExpectations(t) + + mnb := &zigbee.MockProvider{} + defer mnb.AssertExpectations(t) + + md := &mocks.MockDevice{} + defer md.AssertExpectations(t) + + mzc := &mocks2.MockZCLCommunicator{} + defer mzc.AssertExpectations(t) + + mzi.On("NodeBinder").Return(mnb) + mzi.On("ZCLCommunicator").Return(mzc) + + ieee := zigbee.GenerateLocalAdministeredIEEEAddress() + + mzi.On("TransmissionLookup", md, zigbee.ProfileHomeAutomation).Return(ieee, zigbee.Endpoint(2), false, 4) + + mnb.On("BindNodeToController", mock.Anything, ieee, zigbee.Endpoint(2), zigbee.Endpoint(3), zcl.BasicId).Return(nil) + + mzc.On("ConfigureReporting", mock.Anything, ieee, false, zcl.BasicId, zigbee.NoManufacturer, zigbee.Endpoint(2), zigbee.Endpoint(3), uint8(4), basic.ZCLVersion, zcl.TypeUnsignedInt8, uint16(60), uint16(240), 0).Return(nil) + + s := memory.New() + i := NewDeviceWorkaround(mzi) + i.Init(md, s) + + attached, err := i.Enumerate(nil, map[string]any{"EnableZCLReportingKeepAlive": true, "ZigbeeEndpoint": zigbee.Endpoint(3)}) + assert.NoError(t, err) + assert.True(t, attached) + + ws := s.Section("Workarounds") + assert.Contains(t, ws.SectionKeys(), "ZCLReportingKeepAlive") + }) +} diff --git a/implcaps/generic/product_information.go b/implcaps/generic/product_information/impl.go similarity index 57% rename from implcaps/generic/product_information.go rename to implcaps/generic/product_information/impl.go index 6b4cd53..e8a5f45 100644 --- a/implcaps/generic/product_information.go +++ b/implcaps/generic/product_information/impl.go @@ -1,4 +1,4 @@ -package generic +package product_information import ( "context" @@ -10,25 +10,25 @@ import ( "sync" ) -type ProductInformation struct { +type Implementation struct { s persistence.Section m *sync.RWMutex pi *capabilities.ProductInfo } -func NewProductInformation() *ProductInformation { - return &ProductInformation{m: &sync.RWMutex{}} +func NewProductInformation() *Implementation { + return &Implementation{m: &sync.RWMutex{}} } -func (g *ProductInformation) ImplName() string { +func (g *Implementation) ImplName() string { return "GenericProductInformation" } -func (g *ProductInformation) Init(_ da.Device, section persistence.Section) { +func (g *Implementation) Init(_ da.Device, section persistence.Section) { g.s = section } -func (g *ProductInformation) Load(_ context.Context) (bool, error) { +func (g *Implementation) Load(_ context.Context) (bool, error) { g.m.Lock() defer g.m.Unlock() @@ -41,19 +41,20 @@ func (g *ProductInformation) Load(_ context.Context) (bool, error) { return true, nil } -func (g *ProductInformation) Capability() da.Capability { +func (g *Implementation) Capability() da.Capability { return capabilities.ProductInformationFlag } -func (g *ProductInformation) Name() string { +func (g *Implementation) Name() string { return capabilities.StandardNames[capabilities.ProductInformationFlag] } -func (g *ProductInformation) Enumerate(_ context.Context, m map[string]any) (bool, error) { +func (g *Implementation) Enumerate(_ context.Context, m map[string]any) (bool, error) { g.m.Lock() defer g.m.Unlock() newPI := &capabilities.ProductInfo{} + attach := false for k, v := range m { stringV, ok := v.(string) @@ -65,32 +66,39 @@ func (g *ProductInformation) Enumerate(_ context.Context, m map[string]any) (boo case "Name": newPI.Name = stringV g.s.Set("Name", stringV) + attach = true case "Manufacturer": newPI.Manufacturer = stringV g.s.Set("Manufacturer", stringV) + attach = true case "Version": newPI.Version = stringV g.s.Set("Version", stringV) + attach = true case "Serial": newPI.Serial = stringV g.s.Set("Serial", stringV) + attach = true } } - g.pi = newPI - return true, nil + if attach { + g.pi = newPI + } + + return attach, nil } -func (g *ProductInformation) Detach(_ context.Context, _ implcaps.DetachType) error { +func (g *Implementation) Detach(_ context.Context, _ implcaps.DetachType) error { return nil } -func (g *ProductInformation) Get(_ context.Context) (capabilities.ProductInfo, error) { +func (g *Implementation) Get(_ context.Context) (capabilities.ProductInfo, error) { g.m.RLock() defer g.m.RUnlock() return *g.pi, nil } -var _ capabilities.ProductInformation = (*ProductInformation)(nil) -var _ implcaps.ZDACapability = (*ProductInformation)(nil) -var _ da.BasicCapability = (*ProductInformation)(nil) +var _ capabilities.ProductInformation = (*Implementation)(nil) +var _ implcaps.ZDACapability = (*Implementation)(nil) +var _ da.BasicCapability = (*Implementation)(nil) diff --git a/implcaps/generic/product_information_test.go b/implcaps/generic/product_information/impl_test.go similarity index 89% rename from implcaps/generic/product_information_test.go rename to implcaps/generic/product_information/impl_test.go index a7de5d2..e0120f4 100644 --- a/implcaps/generic/product_information_test.go +++ b/implcaps/generic/product_information/impl_test.go @@ -1,4 +1,4 @@ -package generic +package product_information import ( "context" @@ -10,7 +10,7 @@ import ( func TestProductInformation(t *testing.T) { t.Run("has basic capability functions", func(t *testing.T) { - pi := ProductInformation{} + pi := Implementation{} assert.Equal(t, capabilities.ProductInformationFlag, pi.Capability()) assert.Equal(t, capabilities.StandardNames[capabilities.ProductInformationFlag], pi.Name()) @@ -98,4 +98,14 @@ func TestProductInformation(t *testing.T) { assert.Equal(t, out1, out2) }) + + t.Run("fails to attach if there is no data", func(t *testing.T) { + pi := NewProductInformation() + pi.Init(nil, memory.New()) + + attached, err := pi.Enumerate(nil, map[string]any{}) + assert.False(t, attached) + assert.NoError(t, err) + }) + } diff --git a/implcaps/interface.go b/implcaps/interface.go index 7c8f184..588f8b8 100644 --- a/implcaps/interface.go +++ b/implcaps/interface.go @@ -55,6 +55,8 @@ type ZDAInterface interface { NewAttributeMonitor() attribute.Monitor // SendEvent allows a capability to publish event messages. SendEvent(any) + //NodeBinder grants access to the NodeBinder. + NodeBinder() zigbee.NodeBinder //ZCLCommunicator grants access to the ZCL communicator for issuing commands. ZCLCommunicator() communicator.Communicator //ZCLRegister registers a ZCL local command library. diff --git a/implcaps/mock.go b/implcaps/mock.go index 982ddab..c872d96 100644 --- a/implcaps/mock.go +++ b/implcaps/mock.go @@ -14,6 +14,10 @@ type MockZDAInterface struct { mock.Mock } +func (m *MockZDAInterface) NodeBinder() zigbee.NodeBinder { + return m.Called().Get(0).(zigbee.NodeBinder) +} + func (m *MockZDAInterface) Logger() logwrap.Logger { return m.Called().Get(0).(logwrap.Logger) } diff --git a/implcaps/proprietary/tirouter/device_workaround/impl.go b/implcaps/proprietary/tirouter/device_workaround/impl.go deleted file mode 100644 index 71788a4..0000000 --- a/implcaps/proprietary/tirouter/device_workaround/impl.go +++ /dev/null @@ -1,88 +0,0 @@ -package device_workaround - -import ( - "context" - "github.com/shimmeringbee/da" - "github.com/shimmeringbee/da/capabilities" - "github.com/shimmeringbee/persistence" - "github.com/shimmeringbee/zcl" - "github.com/shimmeringbee/zcl/commands/local/basic" - "github.com/shimmeringbee/zda/attribute" - "github.com/shimmeringbee/zda/implcaps" - "github.com/shimmeringbee/zigbee" - "time" -) - -var _ implcaps.ZDACapability = (*Implementation)(nil) - -func NewDeviceWorkaround(zi implcaps.ZDAInterface) *Implementation { - return &Implementation{zi: zi} -} - -type Implementation struct { - s persistence.Section - d da.Device - am attribute.Monitor - zi implcaps.ZDAInterface -} - -func (i *Implementation) Capability() da.Capability { - return capabilities.DeviceWorkaroundFlag -} - -func (i *Implementation) Name() string { - return capabilities.StandardNames[capabilities.DeviceWorkaroundFlag] -} - -func (i *Implementation) Init(d da.Device, s persistence.Section) { - i.d = d - i.s = s - - i.am = i.zi.NewAttributeMonitor() - i.am.Init(s.Section("AttributeMonitor", "ZCLVersion"), d, i.update) -} - -func (i *Implementation) Load(ctx context.Context) (bool, error) { - if err := i.am.Load(ctx); err != nil { - return false, err - } else { - return true, nil - } -} - -func (i *Implementation) Enumerate(ctx context.Context, m map[string]any) (bool, error) { - endpoint := implcaps.Get(m, "ZigbeeEndpoint", zigbee.Endpoint(1)) - - reporting := attribute.ReportingConfig{ - Mode: attribute.AttemptConfigureReporting, - MinimumInterval: 1 * time.Minute, - MaximumInterval: 2 * time.Minute, - ReportableChange: uint(0), - } - - polling := attribute.PollingConfig{ - Mode: attribute.NeverPoll, - } - - if err := i.am.Attach(ctx, endpoint, zcl.BasicId, basic.ZCLVersion, zcl.TypeUnsignedInt8, reporting, polling); err != nil { - return false, err - } - - return true, nil -} - -func (i *Implementation) Detach(ctx context.Context, detachType implcaps.DetachType) error { - if err := i.am.Detach(ctx, detachType == implcaps.NoLongerEnumerated); err != nil { - return err - } - - return nil -} - -func (i *Implementation) ImplName() string { - return "ProprietaryTiRouterDeviceWorkaround" -} - -func (i *Implementation) update(_ zcl.AttributeID, _ zcl.AttributeDataTypeValue) { - -} diff --git a/implcaps/proprietary/tirouter/device_workaround/impl_test.go b/implcaps/proprietary/tirouter/device_workaround/impl_test.go deleted file mode 100644 index 349748f..0000000 --- a/implcaps/proprietary/tirouter/device_workaround/impl_test.go +++ /dev/null @@ -1,125 +0,0 @@ -package device_workaround - -import ( - "context" - "github.com/shimmeringbee/da/capabilities" - "github.com/shimmeringbee/da/mocks" - "github.com/shimmeringbee/persistence/impl/memory" - "github.com/shimmeringbee/zcl" - "github.com/shimmeringbee/zcl/commands/local/basic" - "github.com/shimmeringbee/zda/attribute" - "github.com/shimmeringbee/zda/implcaps" - "github.com/shimmeringbee/zigbee" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/mock" - "io" - "testing" -) - -func TestImplementation_BaseFunctions(t *testing.T) { - t.Run("basic static functions respond correctly", func(t *testing.T) { - i := NewDeviceWorkaround(nil) - - assert.Equal(t, capabilities.DeviceWorkaroundFlag, i.Capability()) - assert.Equal(t, capabilities.StandardNames[capabilities.DeviceWorkaroundFlag], i.Name()) - assert.Equal(t, "ProprietaryTiRouterDeviceWorkaround", 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) - - 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 := NewDeviceWorkaround(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) { - mm := &attribute.MockMonitor{} - defer mm.AssertExpectations(t) - - mm.On("Load", mock.Anything).Return(nil) - - i := NewDeviceWorkaround(nil) - i.am = 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) { - mm := &attribute.MockMonitor{} - defer mm.AssertExpectations(t) - - mm.On("Load", mock.Anything).Return(io.EOF) - - i := NewDeviceWorkaround(nil) - i.am = mm - attached, err := i.Load(context.TODO()) - - assert.False(t, attached) - assert.Error(t, err) - }) -} - -func TestImplementation_Enumerate(t *testing.T) { - t.Run("attaches to the attribute monitor", func(t *testing.T) { - mm := &attribute.MockMonitor{} - defer mm.AssertExpectations(t) - - mm.On("Attach", mock.Anything, zigbee.Endpoint(0x01), zcl.BasicId, basic.ZCLVersion, zcl.TypeUnsignedInt8, mock.Anything, mock.Anything).Return(nil) - - i := NewDeviceWorkaround(nil) - i.am = mm - attached, err := i.Enumerate(context.TODO(), make(map[string]any)) - - 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) - - mm.On("Attach", mock.Anything, zigbee.Endpoint(0x01), zcl.BasicId, basic.ZCLVersion, zcl.TypeUnsignedInt8, mock.Anything, mock.Anything).Return(io.EOF) - - i := NewDeviceWorkaround(nil) - i.am = mm - attached, err := i.Enumerate(context.TODO(), make(map[string]any)) - - assert.False(t, attached) - assert.Error(t, err) - }) -} - -func TestImplementation_Detach(t *testing.T) { - t.Run("detached attribute monitor on detach", func(t *testing.T) { - mm := &attribute.MockMonitor{} - defer mm.AssertExpectations(t) - - mm.On("Detach", mock.Anything, true).Return(nil) - - i := NewDeviceWorkaround(nil) - i.am = mm - - err := i.Detach(context.TODO(), implcaps.NoLongerEnumerated) - assert.NoError(t, err) - }) -} diff --git a/rules/engine.go b/rules/engine.go index 3c942ae..69907d7 100644 --- a/rules/engine.go +++ b/rules/engine.go @@ -125,6 +125,7 @@ func (e *Engine) LoadReader(r io.Reader) error { func (e *Engine) LoadFS(lFS fs.FS) error { return fs.WalkDir(lFS, ".", func(path string, d fs.DirEntry, err error) error { + if !d.IsDir() && strings.HasSuffix(d.Name(), ".json") { if f, err := lFS.Open(d.Name()); err != nil { return err @@ -133,7 +134,9 @@ func (e *Engine) LoadFS(lFS fs.FS) error { _ = f.Close() }() - err = e.LoadReader(f) + if err = e.LoadReader(f); err != nil { + return err + } } } diff --git a/rules/generic.json b/rules/generic.json new file mode 100644 index 0000000..dec68fc --- /dev/null +++ b/rules/generic.json @@ -0,0 +1,34 @@ +{ + "Name": "generic", + "Rules": [ + { + "Filter": "any(values(Product), {.Name != nil })", + "Actions": { + "Capabilities": { + "Add": { + "GenericProductInformation": { + "Name": "find(concat([Product[Self]],values(Product)), {.Name != nil }).Name", + "Manufacturer": "find(concat([Product[Self]],values(Product)), {.Manufacturer != nil }).Manufacturer", + "Version": "find(concat([Product[Self]],values(Product)), {.Version != nil }).Version", + "Serial": "find(concat([Product[Self]],values(Product)), {.Serial != nil }).Serial" + } + } + } + } + }, + { + "Description": "TI Routers", + "Filter": "Product[Self].Name == 'ti.router'", + "Actions": { + "Capabilities": { + "Add": { + "GenericDeviceWorkarounds": { + "ZigbeeEndpoint": "Fn.Endpoint(Self)", + "EnableZCLReportingKeepAlive": "true" + } + } + } + } + } + ] +} \ No newline at end of file diff --git a/rules/proprietary.json b/rules/proprietary.json deleted file mode 100644 index c39cfa8..0000000 --- a/rules/proprietary.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "Name": "proprietary", - "Rules": [ - { - "Description": "TI Routers", - "Filter": "Product[Self].Name == 'ti.router'", - "Actions": { - "Capabilities": { - "Add": { - "ProprietaryTiRouterDeviceWorkaround": { - "ZigbeeEndpoint": "Fn.Endpoint(Self)" - } - } - } - } - } - ] -} \ No newline at end of file diff --git a/rules/xaiomi.json b/rules/xaiomi.json deleted file mode 100644 index 88c63da..0000000 --- a/rules/xaiomi.json +++ /dev/null @@ -1,55 +0,0 @@ -{ - "Name": "xaiomi", - "Rules": [ - { - "Description": "Lumi United Technology Co, Ltd (joined Xiaomi in 2015)", - "Filter": "(Node.ManufacturerCode == 0x1037) and (Product[Self].Manufacturer == 'LUMI')", - "Actions": { - "Capabilities": { - "Add": { - "XiaomiPowerSupply": { - "ZigbeeEndpoint": "Fn.Endpoint(Self)" - } - } - } - }, - "Children": [ - { - "Description": "Xiaomi WSDCGQ01LM", - "Filter": "(Product[Self].Name == 'lumi.sens') and (Endpoint[Self].DeviceID == 0x5F01)", - "Actions": { - "Capabilities": { - "Add": { - "XiaomiTemperatureSensor": { - "ZigbeeEndpoint": "Fn.Endpoint(Self)" - }, - "XiaomiHumiditySensor": { - "ZigbeeEndpoint": "Fn.Endpoint(Self)" - } - } - } - } - }, - { - "Description": "Xiaomi WSDCGQ11LM", - "Filter": "(Product[Self].Name == 'lumi.weather') and (Endpoint[Self].DeviceID == 0x0302)", - "Actions": { - "Capabilities": { - "Add": { - "XiaomiTemperatureSensor": { - "ZigbeeEndpoint": "Fn.Endpoint(Self)" - }, - "XiaomiHumiditySensor": { - "ZigbeeEndpoint": "Fn.Endpoint(Self)" - }, - "XiaomiPressureSensor": { - "ZigbeeEndpoint": "Fn.Endpoint(Self)" - } - } - } - } - } - ] - } - ] -} \ No newline at end of file diff --git a/rules/zcl.json b/rules/zcl.json index 852bad0..5e782a2 100644 --- a/rules/zcl.json +++ b/rules/zcl.json @@ -1,21 +1,9 @@ { "Name": "zcl", + "DependsOn": [ + "generic" + ], "Rules": [ - { - "Filter": "any(values(Product), {.Name != nil })", - "Actions": { - "Capabilities": { - "Add": { - "GenericProductInformation": { - "Name": "find(values(Product), {.Name != nil }).Name", - "Manufacturer": "find(values(Product), {.Manufacturer != nil }).Manufacturer", - "Version": "find(values(Product), {.Version != nil }).Version", - "Serial": "find(values(Product), {.Serial != nil }).Serial" - } - } - } - } - }, { "Filter": "(0x0000 in Endpoint[Self].InClusters || 0x0001 in Endpoint[Self].InClusters)", "Actions": { diff --git a/rules/zstack.json b/rules/zstack.json deleted file mode 100644 index 4fb18d6..0000000 --- a/rules/zstack.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "Name": "zstack", - "DependsOn": [ - "zcl" - ], - "Rules": [ - { - "Description": "CC2XXX Router (https://github.com/Koenkk/Z-Stack-firmware/tree/master/router)", - "Filter": "(Node.ManufacturerCode == 0x115F) and (Product[Self].Manufacturer == 'LUMI') and (Product[Self].Name == 'lumi.router') and (Endpoint[Self].DeviceID == 0x0100)", - "Actions": { - "Capabilities": { - "Remove": { - "ZCLOnOff": {} - } - } - } - } - ] -} \ No newline at end of file diff --git a/table_test.go b/table_test.go index b702696..f07c219 100644 --- a/table_test.go +++ b/table_test.go @@ -5,7 +5,7 @@ import ( "github.com/shimmeringbee/da" "github.com/shimmeringbee/da/capabilities" "github.com/shimmeringbee/persistence/impl/memory" - "github.com/shimmeringbee/zda/implcaps/generic" + "github.com/shimmeringbee/zda/implcaps/generic/product_information" "github.com/shimmeringbee/zigbee" "github.com/stretchr/testify/assert" "testing" @@ -246,7 +246,7 @@ func Test_gateway_attachCapabilityToDevice(t *testing.T) { _ = drainEvents(g) - c := generic.NewProductInformation() + c := product_information.NewProductInformation() g.attachCapabilityToDevice(d, c) assert.Contains(t, d.capabilities, capabilities.ProductInformationFlag) @@ -269,7 +269,7 @@ func Test_gateway_detachCapabilityFromDevice(t *testing.T) { n, _ := g.createNode(addr) d := g.createNextDevice(n) - c := generic.NewProductInformation() + c := product_information.NewProductInformation() g.attachCapabilityToDevice(d, c) assert.True(t, g.sectionForDevice(d.address).Section("Device").SectionExists(capabilities.StandardNames[capabilities.ProductInformationFlag])) @@ -296,7 +296,7 @@ func Test_gateway_detachCapabilityFromDevice(t *testing.T) { n, _ := g.createNode(addr) d := g.createNextDevice(n) - c := generic.NewProductInformation() + c := product_information.NewProductInformation() _ = drainEvents(g) diff --git a/zda_interface.go b/zda_interface.go index a10b5f5..891c322 100644 --- a/zda_interface.go +++ b/zda_interface.go @@ -17,6 +17,10 @@ type zdaInterface struct { c communicator.Communicator } +func (z zdaInterface) NodeBinder() zigbee.NodeBinder { + return z.gw.provider +} + func (z zdaInterface) Logger() logwrap.Logger { return z.gw.logger }