From 9faae4294fdbe24c67055bb02df320e7e62198d6 Mon Sep 17 00:00:00 2001 From: Peter Wood Date: Tue, 19 Mar 2024 21:35:45 +0000 Subject: [PATCH] Add reloading devices and capabilties from persistence, add devices and nodes to persistence. --- da_events_test.go | 5 +- device.go | 1 + enumerate_device.go | 12 ++- enumerate_device_test.go | 9 ++- gateway.go | 7 +- gateway_test.go | 3 +- go.mod | 7 +- go.sum | 54 ++----------- implcaps/generic/product_information.go | 37 ++++++--- implcaps/generic/product_information_test.go | 33 ++++---- implcaps/interface.go | 25 ++---- persistence.go | 47 +++++++++++ persistence_test.go | 83 ++++++++++++++++++++ provider_load.go | 62 +++++++++++++++ provider_load_test.go | 46 +++++++++++ provider_loop.go | 2 + provider_loop_test.go | 21 +++-- table.go | 21 ++++- table_test.go | 41 ++++++---- utils.go | 2 +- 20 files changed, 389 insertions(+), 129 deletions(-) create mode 100644 persistence.go create mode 100644 persistence_test.go create mode 100644 provider_load.go create mode 100644 provider_load_test.go diff --git a/da_events_test.go b/da_events_test.go index 9948671..9438b11 100644 --- a/da_events_test.go +++ b/da_events_test.go @@ -2,6 +2,7 @@ package zda import ( "context" + "github.com/shimmeringbee/persistence/impl/memory" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" "testing" @@ -18,7 +19,7 @@ func (m *mockEventSender) sendEvent(event interface{}) { func TestZigbeeGateway_ReadEvent(t *testing.T) { t.Run("context which expires should result in error", func(t *testing.T) { - zgw := New(context.Background(), nil, nil) + zgw := New(context.Background(), memory.New(), nil, nil) ctx, cancel := context.WithTimeout(context.Background(), 1*time.Millisecond) defer cancel() @@ -28,7 +29,7 @@ func TestZigbeeGateway_ReadEvent(t *testing.T) { }) t.Run("sent events are received through ReadEvent", func(t *testing.T) { - zgw := New(context.Background(), nil, nil).(*gateway) + zgw := New(context.Background(), memory.New(), nil, nil).(*gateway) ctx, cancel := context.WithTimeout(context.Background(), 250*time.Millisecond) defer cancel() diff --git a/device.go b/device.go index 93d10db..6488ebe 100644 --- a/device.go +++ b/device.go @@ -16,6 +16,7 @@ type device struct { m *sync.RWMutex eda *enumeratedDeviceAttachment dr *deviceRemoval + n *node // Mutable data, obtain lock first. deviceId uint16 diff --git a/enumerate_device.go b/enumerate_device.go index 1a4d0a9..27ee96f 100644 --- a/enumerate_device.go +++ b/enumerate_device.go @@ -162,7 +162,7 @@ func (e enumerateDevice) interrogateNode(ctx context.Context, n *node) (inventor } for ep, desc := range inv.endpoints { - if Contains(desc.description.InClusterList, zcl.BasicId) { + if contains(desc.description.InClusterList, zcl.BasicId) { e.logger.LogTrace(ctx, "Querying vendor information from endpoint.", logwrap.Datum("Endpoint", ep)) resp, err := retry.RetryWithValue(ctx, EnumerationNetworkTimeout, EnumerationNetworkRetries, func(ctx context.Context) ([]global.ReadAttributeResponseRecord, error) { @@ -417,10 +417,18 @@ func (e enumerateDevice) enumerateCapabilityOnDevice(ctx context.Context, d *dev e.logger.LogError(ctx, "Failed to find implementation of capability.") return false, []error{fmt.Errorf("failed to find concrete implementation: %s", capImplName)} } + + section := e.gw.sectionForDevice(d.address).Section("capability", capabilities.StandardNames[cF]) + if err := section.Set("implementation", capImplName); err != nil { + e.logger.LogError(ctx, "Failed to set value on capability persistence.", logwrap.Err(err)) + return false, []error{fmt.Errorf("failed to find set value on persistence: %w", err)} + } + + c.Init(d, section.Section("data")) } e.logger.LogInfo(ctx, "Attaching capability implementation.") - attached, err := c.Attach(ctx, d, implcaps.Enumeration, settings) + 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)) errs = append(errs, fmt.Errorf("error while attaching: %s: %w", capImplName, err)) diff --git a/enumerate_device_test.go b/enumerate_device_test.go index f6ea787..d9a256a 100644 --- a/enumerate_device_test.go +++ b/enumerate_device_test.go @@ -6,6 +6,7 @@ import ( "github.com/shimmeringbee/da/capabilities" "github.com/shimmeringbee/logwrap" "github.com/shimmeringbee/logwrap/impl/discard" + "github.com/shimmeringbee/persistence/impl/memory" "github.com/shimmeringbee/zcl" "github.com/shimmeringbee/zcl/commands/global" "github.com/shimmeringbee/zcl/commands/local/basic" @@ -469,7 +470,7 @@ func Test_enumerateDevice_updateCapabilitiesOnDevice(t *testing.T) { t.Run("adds a new capability from rules output", func(t *testing.T) { mdm := &mockDeviceManager{} defer mdm.AssertExpectations(t) - ed := enumerateDevice{logger: logwrap.New(discard.Discard()), capabilityFactory: factory.Create, dm: mdm} + ed := enumerateDevice{logger: logwrap.New(discard.Discard()), capabilityFactory: factory.Create, dm: mdm, gw: &gateway{section: memory.New()}} d := &device{m: &sync.RWMutex{}, deviceId: 1, capabilities: map[da.Capability]implcaps.ZDACapability{}} id := inventoryDevice{ @@ -509,7 +510,8 @@ func Test_enumerateDevice_updateCapabilitiesOnDevice(t *testing.T) { ed := enumerateDevice{logger: logwrap.New(discard.Discard()), capabilityFactory: factory.Create, dm: mdm} opi := generic.NewProductInformation() d := &device{m: &sync.RWMutex{}, deviceId: 1, capabilities: map[da.Capability]implcaps.ZDACapability{capabilities.ProductInformationFlag: opi}} - _, _ = opi.Attach(context.Background(), d, implcaps.Enumeration, map[string]interface{}{ + opi.Init(d, memory.New()) + _, _ = opi.Enumerate(context.Background(), map[string]interface{}{ "Name": "NEXUS-6", }) @@ -548,7 +550,8 @@ func Test_enumerateDevice_updateCapabilitiesOnDevice(t *testing.T) { ed := enumerateDevice{logger: logwrap.New(discard.Discard()), capabilityFactory: factory.Create, dm: mdm} opi := generic.NewProductInformation() d := &device{m: &sync.RWMutex{}, deviceId: 1, capabilities: map[da.Capability]implcaps.ZDACapability{capabilities.ProductInformationFlag: opi}} - _, _ = opi.Attach(context.Background(), d, implcaps.Enumeration, map[string]interface{}{ + opi.Init(d, memory.New()) + _, _ = opi.Enumerate(context.Background(), map[string]interface{}{ "Name": "NEXUS-6", }) d.capabilities[capabilities.ProductInformationFlag] = opi diff --git a/gateway.go b/gateway.go index 8ea0399..cf78810 100644 --- a/gateway.go +++ b/gateway.go @@ -6,6 +6,7 @@ import ( "github.com/shimmeringbee/da" "github.com/shimmeringbee/da/capabilities" "github.com/shimmeringbee/logwrap" + "github.com/shimmeringbee/persistence" "github.com/shimmeringbee/zcl" "github.com/shimmeringbee/zcl/commands/global" "github.com/shimmeringbee/zcl/communicator" @@ -19,7 +20,7 @@ import ( const DefaultGatewayHomeAutomationEndpoint = zigbee.Endpoint(0x01) -func New(baseCtx context.Context, p zigbee.Provider, r ruleExecutor) da.Gateway { +func New(baseCtx context.Context, s persistence.Section, p zigbee.Provider, r ruleExecutor) da.Gateway { ctx, cancel := context.WithCancel(baseCtx) zclCommandRegistry := zcl.NewCommandRegistry() @@ -39,6 +40,8 @@ func New(baseCtx context.Context, p zigbee.Provider, r ruleExecutor) da.Gateway nodeLock: &sync.RWMutex{}, node: make(map[zigbee.IEEEAddress]*node), + section: s, + callbacks: callbacks.Create(), ruleExecutor: r, @@ -83,6 +86,8 @@ type gateway struct { nodeLock *sync.RWMutex node map[zigbee.IEEEAddress]*node + section persistence.Section + callbacks callbacks.AdderCaller ruleExecutor ruleExecutor diff --git a/gateway_test.go b/gateway_test.go index 90dfbbe..ef5e20b 100644 --- a/gateway_test.go +++ b/gateway_test.go @@ -5,6 +5,7 @@ import ( "github.com/shimmeringbee/da/capabilities" "github.com/shimmeringbee/logwrap" "github.com/shimmeringbee/logwrap/impl/discard" + "github.com/shimmeringbee/persistence/impl/memory" "github.com/shimmeringbee/zigbee" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" @@ -23,7 +24,7 @@ func newTestGateway() (*gateway, *zigbee.MockProvider, *mock.Call, func(*testing mRE := mp.On("ReadEvent", mock.Anything).Return(nil, context.Canceled).Maybe() - gw := New(context.Background(), mp, nil) + gw := New(context.Background(), memory.New(), mp, nil) gw.(*gateway).WithLogWrapLogger(logwrap.New(discard.Discard())) diff --git a/go.mod b/go.mod index 9ea9015..78adf1c 100644 --- a/go.mod +++ b/go.mod @@ -1,16 +1,17 @@ module github.com/shimmeringbee/zda -go 1.22 +go 1.22.0 require ( github.com/antonmedv/expr v1.15.5 github.com/shimmeringbee/callbacks v0.0.0-20221001135028-b85b5f89d5d6 github.com/shimmeringbee/da v0.0.0-20240114214251-b3b74f3b1894 github.com/shimmeringbee/logwrap v0.1.3 + github.com/shimmeringbee/persistence v0.0.0-20240318205009-b8100e7c3887 github.com/shimmeringbee/retry v0.0.0-20221006193055-2ce01bf139c2 github.com/shimmeringbee/zcl v0.0.0-20221006205348-732be4c63285 github.com/shimmeringbee/zigbee v0.0.0-20221016122511-6c2328db0d94 - github.com/stretchr/testify v1.8.4 + github.com/stretchr/testify v1.9.0 golang.org/x/sync v0.5.0 ) @@ -19,6 +20,6 @@ require ( github.com/lucasb-eyer/go-colorful v1.2.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/shimmeringbee/bytecodec v0.0.0-20210228205504-1e9e0677347b // indirect - github.com/stretchr/objx v0.5.1 // indirect + github.com/stretchr/objx v0.5.2 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 2e2a849..22dd8a4 100644 --- a/go.sum +++ b/go.sum @@ -1,44 +1,27 @@ -github.com/DATA-DOG/go-sqlmock v1.3.3/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM= -github.com/antonmedv/expr v1.9.0 h1:j4HI3NHEdgDnN9p6oI6Ndr0G5QryMY0FNxT4ONrFDGU= -github.com/antonmedv/expr v1.9.0/go.mod h1:5qsM3oLGDND7sDmQGDXHkYfkjYMUX14qsgqmHhwGEk8= -github.com/antonmedv/expr v1.9.1-0.20221112134005-497c6bd01cf8 h1:ANpbgpXXI0gdonf79fgpVou3hdgtvGmN2F1qLvY6F/w= -github.com/antonmedv/expr v1.9.1-0.20221112134005-497c6bd01cf8/go.mod h1:FPC8iWArxls7axbVLsW+kpg1mz29A1b2M6jt+hZfDkU= github.com/antonmedv/expr v1.15.5 h1:y0Iz3cEwmpRz5/r3w4qQR0MfIqJGdGM1zbhD/v0G5Vg= github.com/antonmedv/expr v1.15.5/go.mod h1:0E/6TxnOlRNp81GMzX9QfDPAmHo2Phg00y4JUv1ihsE= -github.com/davecgh/go-spew v0.0.0-20161028175848-04cdfd42973b/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg= -github.com/gdamore/tcell v1.3.0/go.mod h1:Hjvr+Ofd+gLglo7RYKxxnzCBmev3BzsS67MebKS4zMM= github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/lucasb-eyer/go-colorful v1.0.2/go.mod h1:0MS4r+7BZKSJ5mw4/S5MPN+qHFF1fYclkSPilDOKW0s= github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY= github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= -github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= -github.com/mattn/go-runewidth v0.0.8/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI= -github.com/pmezard/go-difflib v0.0.0-20151028094244-d8ed2627bdf0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 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/rivo/tview v0.0.0-20200219210816-cd38d7432498/go.mod h1:6lkG1x+13OShEf0EaOCaTQYyB7d5nSbb181KtjlS+84= -github.com/rivo/uniseg v0.1.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= -github.com/sanity-io/litter v1.2.0/go.mod h1:JF6pZUFgu2Q0sBZ+HSV35P8TVPI1TTzEwyu9FXAw2W4= github.com/shimmeringbee/bytecodec v0.0.0-20201107142444-94bb5c0baaee/go.mod h1:WYnxfxTJ45UQ+xeAuuTSIalcEepgP8Rb7T/OhCaDdgo= 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/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/da v0.0.0-20231129204318-63ea834a97fd h1:C82d6yE2DWdS0PEQlyL/yIhn4Ozzf5mQWX0TbrQ4kno= -github.com/shimmeringbee/da v0.0.0-20231129204318-63ea834a97fd/go.mod h1:6Oji+jlg9NfJrlAcsGduhcQVffH7v57IRHguPF1JmgA= -github.com/shimmeringbee/da v0.0.0-20231129212915-774722ece3c7 h1:0ihLdGeXyzpNHbKKNQWjmS7ZpQy6cCiEH8Hz+gqvSh4= -github.com/shimmeringbee/da v0.0.0-20231129212915-774722ece3c7/go.mod h1:jUKTa353LvJT3TAdwtmfGEbcxkYbG58h0gbASRf0FIs= -github.com/shimmeringbee/da v0.0.0-20231216201234-d76445766fbb h1:7ShcdpIoUb7z7/rW/lTXBgWhjjsHsry9dBIL9vpQXFg= -github.com/shimmeringbee/da v0.0.0-20231216201234-d76445766fbb/go.mod h1:jUKTa353LvJT3TAdwtmfGEbcxkYbG58h0gbASRf0FIs= github.com/shimmeringbee/da v0.0.0-20240114214251-b3b74f3b1894 h1:Q7vXhYoHVeh3hFHlAFL1VPbPlhVDwvCnKjtu8VPbUz4= github.com/shimmeringbee/da v0.0.0-20240114214251-b3b74f3b1894/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-20240311211126-71abe465da0c h1:pxfug23MwBm39D6JKXoykQRSCwUNlr4FbsKqphh/qjU= +github.com/shimmeringbee/persistence v0.0.0-20240311211126-71abe465da0c/go.mod h1:dKKEj8uVcBM/CArQRE4yLw5DFRfAAzabI7mbgD1ZLeI= +github.com/shimmeringbee/persistence v0.0.0-20240318205009-b8100e7c3887 h1:hr+n6DzP37QYzCf3FN8bcuPoNSsbQe3u4QztdHjOq3I= +github.com/shimmeringbee/persistence v0.0.0-20240318205009-b8100e7c3887/go.mod h1:dKKEj8uVcBM/CArQRE4yLw5DFRfAAzabI7mbgD1ZLeI= 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/zcl v0.0.0-20221006205348-732be4c63285 h1:GGO7oZJf4q7fA5yimMnfHjgOqq6WJOQX9DSBzxm0xBM= @@ -49,38 +32,17 @@ github.com/shimmeringbee/zigbee v0.0.0-20221016122511-6c2328db0d94/go.mod h1:GMA github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.3.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= -github.com/stretchr/objx v0.4.0 h1:M2gUjqZET1qApGOWNSnZ49BAIMX4F/1plDv3+l31EJ4= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0 h1:1zr/of2m5FGMsad5YfcqgdqdWrIhu+EBEJRhR1U7z/c= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/objx v0.5.1 h1:4VhoImhV/Bm0ToFkXFi8hXNXwpDRZ/ynw3amt82mzq0= -github.com/stretchr/objx v0.5.1/go.mod h1:/iHQpkQwBD6DLUmQ4pE+s1TXdob1mORJ4/UFdrifcy0= -github.com/stretchr/testify v0.0.0-20161117074351-18a02ba4a312/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -golang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0 h1:cu5kTvlzcw1Q5S9f5ip1/cpiB4nXvw1XYzFPGgzLUOY= -golang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= -golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE= golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= diff --git a/implcaps/generic/product_information.go b/implcaps/generic/product_information.go index 5c2c645..6b40f0d 100644 --- a/implcaps/generic/product_information.go +++ b/implcaps/generic/product_information.go @@ -5,11 +5,13 @@ import ( "fmt" "github.com/shimmeringbee/da" "github.com/shimmeringbee/da/capabilities" + "github.com/shimmeringbee/persistence" "github.com/shimmeringbee/zda/implcaps" "sync" ) type ProductInformation struct { + s persistence.Section m *sync.RWMutex pi *capabilities.ProductInfo } @@ -22,6 +24,23 @@ func (g *ProductInformation) ImplName() string { return "GenericProductInformation" } +func (g *ProductInformation) Init(_ da.Device, section persistence.Section) { + g.s = section +} + +func (g *ProductInformation) Load(_ context.Context) (bool, error) { + g.m.Lock() + defer g.m.Unlock() + + g.pi = &capabilities.ProductInfo{} + g.pi.Name, _ = g.s.String("Name") + g.pi.Manufacturer, _ = g.s.String("Manufacturer") + g.pi.Version, _ = g.s.String("Version") + g.pi.Serial, _ = g.s.String("Serial") + + return true, nil +} + func (g *ProductInformation) Capability() da.Capability { return capabilities.ProductInformationFlag } @@ -30,7 +49,7 @@ func (g *ProductInformation) Name() string { return capabilities.StandardNames[capabilities.ProductInformationFlag] } -func (g *ProductInformation) Attach(_ context.Context, _ da.Device, _ implcaps.AttachType, m map[string]interface{}) (bool, error) { +func (g *ProductInformation) Enumerate(_ context.Context, m map[string]interface{}) (bool, error) { g.m.Lock() defer g.m.Unlock() @@ -45,12 +64,16 @@ func (g *ProductInformation) Attach(_ context.Context, _ da.Device, _ implcaps.A switch k { case "Name": newPI.Name = stringV + _ = g.s.Set("Name", stringV) case "Manufacturer": newPI.Manufacturer = stringV + _ = g.s.Set("Manufacturer", stringV) case "Version": newPI.Version = stringV + _ = g.s.Set("Version", stringV) case "Serial": newPI.Serial = stringV + _ = g.s.Set("Serial", stringV) } } @@ -62,18 +85,6 @@ func (g *ProductInformation) Detach(_ context.Context, _ implcaps.DetachType) er return nil } -func (g *ProductInformation) State() map[string]interface{} { - g.m.RLock() - defer g.m.RUnlock() - - return map[string]interface{}{ - "Name": g.pi.Name, - "Serial": g.pi.Serial, - "Manufacturer": g.pi.Manufacturer, - "Version": g.pi.Version, - } -} - func (g *ProductInformation) Get(_ context.Context) (capabilities.ProductInfo, error) { g.m.RLock() defer g.m.RUnlock() diff --git a/implcaps/generic/product_information_test.go b/implcaps/generic/product_information_test.go index 4c16d5d..71c5fbe 100644 --- a/implcaps/generic/product_information_test.go +++ b/implcaps/generic/product_information_test.go @@ -1,10 +1,10 @@ package generic import ( + "context" "github.com/shimmeringbee/da/capabilities" - "github.com/shimmeringbee/zda/implcaps" + "github.com/shimmeringbee/persistence/impl/memory" "github.com/stretchr/testify/assert" - "sync" "testing" ) @@ -18,9 +18,10 @@ func TestProductInformation(t *testing.T) { }) t.Run("accepts data on attach and returns via Get", func(t *testing.T) { - pi := ProductInformation{m: &sync.RWMutex{}} + pi := NewProductInformation() + pi.Init(nil, memory.New()) - attached, err := pi.Attach(nil, nil, implcaps.Enumeration, map[string]interface{}{ + attached, err := pi.Enumerate(nil, map[string]interface{}{ "Name": "NEXUS-7", "Manufacturer": "Tyrell Corporation", "Serial": "N7FAA52318", @@ -42,9 +43,10 @@ func TestProductInformation(t *testing.T) { }) t.Run("handles failure of data gracefully on new enumeration", func(t *testing.T) { - pi := ProductInformation{m: &sync.RWMutex{}} + pi := NewProductInformation() + pi.Init(nil, memory.New()) - attached, err := pi.Attach(nil, nil, implcaps.Enumeration, map[string]interface{}{ + attached, err := pi.Enumerate(nil, map[string]interface{}{ "Name": "NEXUS-7", "Manufacturer": "Tyrell Corporation", "Serial": "N7FAA52318", @@ -52,7 +54,7 @@ func TestProductInformation(t *testing.T) { assert.True(t, attached) assert.NoError(t, err) - attached, err = pi.Attach(nil, nil, implcaps.Enumeration, map[string]interface{}{ + attached, err = pi.Enumerate(nil, map[string]interface{}{ "Name": 7, }) assert.True(t, attached) @@ -60,9 +62,10 @@ func TestProductInformation(t *testing.T) { }) t.Run("fails to attach if data is not string", func(t *testing.T) { - pi := ProductInformation{m: &sync.RWMutex{}} + pi := NewProductInformation() + pi.Init(nil, memory.New()) - attached, err := pi.Attach(nil, nil, implcaps.Enumeration, map[string]interface{}{ + attached, err := pi.Enumerate(nil, map[string]interface{}{ "Name": 7, }) assert.False(t, attached) @@ -70,9 +73,11 @@ func TestProductInformation(t *testing.T) { }) t.Run("Capturing state and reloading should result in same output state", func(t *testing.T) { - pi1 := ProductInformation{m: &sync.RWMutex{}} + s := memory.New() + pi1 := NewProductInformation() + pi1.Init(nil, s) - attached, err := pi1.Attach(nil, nil, implcaps.Enumeration, map[string]interface{}{ + attached, err := pi1.Enumerate(nil, map[string]interface{}{ "Name": "NEXUS-7", "Manufacturer": "Tyrell Corporation", "Serial": "N7FAA52318", @@ -81,10 +86,10 @@ func TestProductInformation(t *testing.T) { assert.True(t, attached) assert.NoError(t, err) - state := pi1.State() + pi2 := NewProductInformation() + pi2.Init(nil, s) - pi2 := ProductInformation{m: &sync.RWMutex{}} - attached, err = pi2.Attach(nil, nil, implcaps.Load, state) + attached, err = pi2.Load(context.TODO()) assert.True(t, attached) assert.NoError(t, err) diff --git a/implcaps/interface.go b/implcaps/interface.go index 166a7f9..198b317 100644 --- a/implcaps/interface.go +++ b/implcaps/interface.go @@ -3,6 +3,7 @@ package implcaps import ( "context" "github.com/shimmeringbee/da" + "github.com/shimmeringbee/persistence" ) const ( @@ -12,16 +13,6 @@ const ( DataKeyZCLAttributeID = "ZCLAttributeID" ) -type AttachType int - -const ( - // Enumeration is used for Attach when the capability is being created or updated through enumerateDevice. - Enumeration AttachType = iota - // Load is used to indicate that state is being loaded from disk. Any Zigbee network configuration should - // be assumed to be complete. - Load -) - type DetachType int const ( @@ -38,18 +29,18 @@ const ( type ZDACapability interface { // BasicCapability functions should also be present. da.BasicCapability - // Attach is used to initial create, re-enumerate or load a capability on a device. The AttachType guides - // the capability in determining what to do. Attach should return true if everything is successful and the - // capability should be attached, or false if it should not. It should also return false if the device has + // Init is used upon creation of the capability to provide persistence. + Init(da.Device, persistence.Section) + // Load is used upon load of the capability from persistence at start up. + Load(context.Context) (bool, error) + // Enumerate is used to enumerate or re-enumerate a device. Attach should return true if everything is successful + // and the capability should be attached, or false if it should not. It should also return false if the device has // now detached as a result of Enumeration. A return value of true and error is possible, and the capability // should attach. - Attach(context.Context, da.Device, AttachType, map[string]interface{}) (bool, error) + Enumerate(context.Context, map[string]interface{}) (bool, error) // Detach is called when a capability is removed from a device. This will be called after an Attach that returned // false, even if it was a new enumeration. Detach(context.Context, DetachType) error - // State returns a data structure that should be passed to Attach with AttachType.LOAD to reload the capability - // from a persistent store. - State() map[string]interface{} // ImplName returns the implementation name of the capability. ImplName() string } diff --git a/persistence.go b/persistence.go new file mode 100644 index 0000000..1efbc7a --- /dev/null +++ b/persistence.go @@ -0,0 +1,47 @@ +package zda + +import ( + "github.com/shimmeringbee/persistence" + "github.com/shimmeringbee/zigbee" + "strconv" +) + +func (g *gateway) sectionRemoveNode(i zigbee.IEEEAddress) bool { + return g.section.Section("node").Delete(i.String()) +} + +func (g *gateway) sectionForNode(i zigbee.IEEEAddress) persistence.Section { + return g.section.Section("node", i.String()) +} + +func (g *gateway) nodeListFromPersistence() []zigbee.IEEEAddress { + var nodeList []zigbee.IEEEAddress + + for _, k := range g.section.Section("node").Keys() { + if addr, err := strconv.ParseUint(k, 16, 64); err == nil { + nodeList = append(nodeList, zigbee.IEEEAddress(addr)) + } + } + + return nodeList +} + +func (g *gateway) sectionRemoveDevice(i IEEEAddressWithSubIdentifier) bool { + return g.sectionForNode(i.IEEEAddress).Section("device").Delete(strconv.Itoa(int(i.SubIdentifier))) +} + +func (g *gateway) sectionForDevice(i IEEEAddressWithSubIdentifier) persistence.Section { + return g.sectionForNode(i.IEEEAddress).Section("device", strconv.Itoa(int(i.SubIdentifier))) +} + +func (g *gateway) deviceListFromPersistence(id zigbee.IEEEAddress) []IEEEAddressWithSubIdentifier { + var deviceList []IEEEAddressWithSubIdentifier + + for _, k := range g.sectionForNode(id).Section("device").Keys() { + if i, err := strconv.Atoi(k); err == nil { + deviceList = append(deviceList, IEEEAddressWithSubIdentifier{IEEEAddress: id, SubIdentifier: uint8(i)}) + } + } + + return deviceList +} diff --git a/persistence_test.go b/persistence_test.go new file mode 100644 index 0000000..f7a9a03 --- /dev/null +++ b/persistence_test.go @@ -0,0 +1,83 @@ +package zda + +import ( + "context" + "github.com/shimmeringbee/persistence/impl/memory" + "github.com/shimmeringbee/zigbee" + "github.com/stretchr/testify/assert" + "testing" +) + +func Test_gateway_deviceListFromPersistence(t *testing.T) { + t.Run("multiple devices are returned", func(t *testing.T) { + zgw := New(context.Background(), memory.New(), nil, nil).(*gateway) + + ieee := zigbee.GenerateLocalAdministeredIEEEAddress() + deviceOne := IEEEAddressWithSubIdentifier{IEEEAddress: ieee, SubIdentifier: 1} + deviceTwo := IEEEAddressWithSubIdentifier{IEEEAddress: ieee, SubIdentifier: 2} + + zgw.sectionForDevice(deviceOne) + zgw.sectionForDevice(deviceTwo) + + devices := zgw.deviceListFromPersistence(ieee) + + assert.Contains(t, devices, deviceOne) + assert.Contains(t, devices, deviceTwo) + }) +} + +func Test_gateway_nodeListFromPersistence(t *testing.T) { + t.Run("multiple nodes are returned", func(t *testing.T) { + zgw := New(context.Background(), memory.New(), nil, nil).(*gateway) + + ieeeOne := zigbee.GenerateLocalAdministeredIEEEAddress() + ieeeTwo := zigbee.GenerateLocalAdministeredIEEEAddress() + + zgw.sectionForNode(ieeeOne) + zgw.sectionForNode(ieeeTwo) + + nodes := zgw.nodeListFromPersistence() + + assert.Contains(t, nodes, ieeeOne) + assert.Contains(t, nodes, ieeeTwo) + }) +} + +func Test_gateway_sectionRemoveNode(t *testing.T) { + t.Run("nodes are removed", func(t *testing.T) { + zgw := New(context.Background(), memory.New(), nil, nil).(*gateway) + + ieeeOne := zigbee.GenerateLocalAdministeredIEEEAddress() + ieeeTwo := zigbee.GenerateLocalAdministeredIEEEAddress() + + zgw.sectionForNode(ieeeOne) + zgw.sectionForNode(ieeeTwo) + + assert.True(t, zgw.sectionRemoveNode(ieeeTwo)) + + nodes := zgw.nodeListFromPersistence() + + assert.Contains(t, nodes, ieeeOne) + assert.NotContains(t, nodes, ieeeTwo) + }) +} + +func Test_gateway_sectionRemoveDevice(t *testing.T) { + t.Run("devices is removed", func(t *testing.T) { + zgw := New(context.Background(), memory.New(), nil, nil).(*gateway) + + ieee := zigbee.GenerateLocalAdministeredIEEEAddress() + deviceOne := IEEEAddressWithSubIdentifier{IEEEAddress: ieee, SubIdentifier: 1} + deviceTwo := IEEEAddressWithSubIdentifier{IEEEAddress: ieee, SubIdentifier: 2} + + zgw.sectionForDevice(deviceOne) + zgw.sectionForDevice(deviceTwo) + + assert.True(t, zgw.sectionRemoveDevice(deviceTwo)) + + devices := zgw.deviceListFromPersistence(ieee) + + assert.Contains(t, devices, deviceOne) + assert.NotContains(t, devices, deviceTwo) + }) +} diff --git a/provider_load.go b/provider_load.go new file mode 100644 index 0000000..3445358 --- /dev/null +++ b/provider_load.go @@ -0,0 +1,62 @@ +package zda + +import ( + "context" + "github.com/shimmeringbee/logwrap" + "github.com/shimmeringbee/zda/implcaps/factory" + "github.com/shimmeringbee/zigbee" +) + +func (g *gateway) providerLoad() { + ctx, end := g.logger.Segment(g.ctx, "Loading persistence.") + defer end() + + for _, i := range g.nodeListFromPersistence() { + g.providerLoadNode(ctx, i) + } +} + +func (g *gateway) providerLoadNode(pctx context.Context, i zigbee.IEEEAddress) { + ctx, end := g.logger.Segment(pctx, "Loading node data.", logwrap.Datum("node", i.String())) + defer end() + + n, _ := g.createNode(i) + for _, d := range g.deviceListFromPersistence(i) { + g.providerLoadDevice(ctx, n, d) + } +} + +func (g *gateway) providerLoadDevice(pctx context.Context, n *node, i IEEEAddressWithSubIdentifier) { + ctx, end := g.logger.Segment(pctx, "Loading device data.", logwrap.Datum("device", i.String())) + defer end() + + d := g.createSpecificDevice(n, i.SubIdentifier) + + capSection := g.sectionForDevice(i).Section("capability") + + for _, cName := range capSection.Keys() { + cSection := capSection.Section(cName) + + if capImpl, ok := cSection.String("implementation"); ok { + if capI := factory.Create(capImpl); capI == nil { + g.logger.LogError(ctx, "Could not find capability implementation.", logwrap.Datum("implementation", capImpl)) + continue + } else { + g.logger.LogInfo(ctx, "Constructed capability implementation.", logwrap.Datum("implementation", capImpl)) + capI.Init(d, cSection.Section("data")) + attached, err := capI.Load(ctx) + + if err != nil { + g.logger.LogError(ctx, "Error while loading from persistence.", logwrap.Err(err), logwrap.Datum("implementation", capImpl)) + } + + if attached { + g.attachCapabilityToDevice(d, capI) + g.logger.LogInfo(ctx, "Attached capability from persistence.", logwrap.Datum("implementation", capImpl)) + } else { + g.logger.LogWarn(ctx, "Rejected capability attach from persistence.", logwrap.Datum("implementation", capImpl)) + } + } + } + } +} diff --git a/provider_load_test.go b/provider_load_test.go new file mode 100644 index 0000000..da6bd3b --- /dev/null +++ b/provider_load_test.go @@ -0,0 +1,46 @@ +package zda + +import ( + "context" + "github.com/shimmeringbee/da/capabilities" + "github.com/shimmeringbee/persistence/impl/memory" + "github.com/shimmeringbee/zigbee" + "github.com/stretchr/testify/assert" + "testing" +) + +func Test_gateway_providerLoad(t *testing.T) { + t.Run("loads node, device and capability from persistence", func(t *testing.T) { + s := memory.New() + + g := New(context.Background(), s, nil, nil).(*gateway) + + id := IEEEAddressWithSubIdentifier{IEEEAddress: zigbee.GenerateLocalAdministeredIEEEAddress(), SubIdentifier: 1} + dS := g.sectionForDevice(id) + + cS := dS.Section("capability", "ProductInformation") + _ = cS.Set("implementation", "GenericProductInformation") + + daS := cS.Section("data") + _ = daS.Set("Name", "NEXUS-7") + _ = daS.Set("Manufacturer", "Tyrell Corporation") + _ = daS.Set("Serial", "N7FAA52318") + _ = daS.Set("Version", "1.0.0") + + g.providerLoad() + + d := g.getDevice(id) + + c := d.Capability(capabilities.ProductInformationFlag) + assert.NotNil(t, c) + + cc := c.(capabilities.ProductInformation) + pi, err := cc.Get(context.Background()) + assert.NoError(t, err) + + assert.Equal(t, "NEXUS-7", pi.Name) + assert.Equal(t, "Tyrell Corporation", pi.Manufacturer) + assert.Equal(t, "N7FAA52318", pi.Serial) + assert.Equal(t, "1.0.0", pi.Version) + }) +} diff --git a/provider_loop.go b/provider_loop.go index e6f69ac..d2a7da9 100644 --- a/provider_loop.go +++ b/provider_loop.go @@ -8,6 +8,8 @@ import ( ) func (g *gateway) providerLoop() { + g.providerLoad() + for { event, err := g.provider.ReadEvent(g.ctx) diff --git a/provider_loop_test.go b/provider_loop_test.go index c585b39..44c853a 100644 --- a/provider_loop_test.go +++ b/provider_loop_test.go @@ -4,6 +4,7 @@ import ( "context" "github.com/shimmeringbee/logwrap" "github.com/shimmeringbee/logwrap/impl/discard" + "github.com/shimmeringbee/persistence/impl/memory" "github.com/shimmeringbee/zigbee" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/mock" @@ -17,7 +18,7 @@ func Test_gateway_receiveNodeJoinEvent(t *testing.T) { mp.On("QueryNodeDescription", mock.Anything, mock.Anything).Return(zigbee.NodeDescription{}, io.EOF).Maybe() defer mp.AssertExpectations(t) - g := New(context.Background(), mp, nil).(*gateway) + g := New(context.Background(), memory.New(), mp, nil).(*gateway) g.WithLogWrapLogger(logwrap.New(discard.Discard())) addr := zigbee.GenerateLocalAdministeredIEEEAddress() @@ -45,17 +46,23 @@ func Test_gateway_receiveNodeJoinEvent(t *testing.T) { assert.NotNil(t, d) assert.True(t, called) + + assert.Contains(t, g.nodeListFromPersistence(), addr) + assert.Contains(t, g.deviceListFromPersistence(addr), d.address) }) } func Test_gateway_receiveNodeLeaveEvent(t *testing.T) { t.Run("node leave event will remove the node from the node table, removing any devices", func(t *testing.T) { - g := New(context.Background(), nil, nil).(*gateway) + g := New(context.Background(), memory.New(), nil, nil).(*gateway) g.WithLogWrapLogger(logwrap.New(discard.Discard())) addr := zigbee.GenerateLocalAdministeredIEEEAddress() n, _ := g.createNode(addr) - _ = g.createNextDevice(n) + d := g.createNextDevice(n) + + assert.Contains(t, g.nodeListFromPersistence(), addr) + assert.Contains(t, g.deviceListFromPersistence(addr), d.address) g.receiveNodeLeaveEvent(zigbee.NodeLeaveEvent{ Node: zigbee.Node{ @@ -66,10 +73,10 @@ func Test_gateway_receiveNodeLeaveEvent(t *testing.T) { assert.Nil(t, g.getNode(addr)) assert.Empty(t, n.device) - d := g.getDevice(IEEEAddressWithSubIdentifier{ - IEEEAddress: addr, - SubIdentifier: 0, - }) + assert.NotContains(t, g.nodeListFromPersistence(), addr) + assert.NotContains(t, g.deviceListFromPersistence(addr), d.address) + + d = g.getDevice(d.address) assert.Nil(t, d) }) diff --git a/table.go b/table.go index ce73b89..c6e11bc 100644 --- a/table.go +++ b/table.go @@ -26,6 +26,8 @@ func (g *gateway) createNode(addr zigbee.IEEEAddress) (*node, bool) { } g.node[addr] = n + + g.sectionForNode(n.address) } return n, !found @@ -45,6 +47,7 @@ func (g *gateway) removeNode(addr zigbee.IEEEAddress) bool { _, found := g.node[addr] if found { delete(g.node, addr) + g.sectionRemoveNode(addr) } return found @@ -89,24 +92,25 @@ func (g *gateway) getDevicesOnNode(n *node) []*device { return devices } -func (g *gateway) createNextDevice(n *node) *device { +func (g *gateway) createSpecificDevice(n *node, subId uint8) *device { n.m.Lock() defer n.m.Unlock() - subId := n._nextDeviceSubIdentifier() - d := &device{ address: IEEEAddressWithSubIdentifier{ IEEEAddress: n.address, SubIdentifier: subId, }, gw: g, + n: n, m: &sync.RWMutex{}, capabilities: make(map[da.Capability]implcaps.ZDACapability), } n.device[subId] = d + g.sectionForDevice(d.address) + d.eda = &enumeratedDeviceAttachment{ node: n, device: d, @@ -128,6 +132,14 @@ func (g *gateway) createNextDevice(n *node) *device { return d } +func (g *gateway) createNextDevice(n *node) *device { + n.m.Lock() + subId := n._nextDeviceSubIdentifier() + n.m.Unlock() + + return g.createSpecificDevice(n, subId) +} + func (g *gateway) removeDevice(ctx context.Context, addr IEEEAddressWithSubIdentifier) bool { n := g.getNode(addr.IEEEAddress) @@ -155,6 +167,7 @@ func (g *gateway) removeDevice(ctx context.Context, addr IEEEAddressWithSubIdent g.sendEvent(da.DeviceRemoved{Device: d}) delete(n.device, addr.SubIdentifier) + g.sectionRemoveDevice(d.address) return true } @@ -165,6 +178,7 @@ func (g *gateway) attachCapabilityToDevice(d *device, c implcaps.ZDACapability) cF := c.Capability() d.capabilities[cF] = c + g.sectionForDevice(d.address).Section("capability", capabilities.StandardNames[cF]) g.sendEvent(da.CapabilityAdded{Device: d, Capability: cF}) } @@ -172,6 +186,7 @@ func (g *gateway) detachCapabilityFromDevice(d *device, c implcaps.ZDACapability cF := c.Capability() if _, found := d.capabilities[cF]; found { g.sendEvent(da.CapabilityRemoved{Device: d, Capability: cF}) + g.sectionForDevice(d.address).Section("capability").Delete(capabilities.StandardNames[cF]) delete(d.capabilities, cF) } } diff --git a/table_test.go b/table_test.go index d77eaae..c273d09 100644 --- a/table_test.go +++ b/table_test.go @@ -4,6 +4,7 @@ import ( "context" "github.com/shimmeringbee/da" "github.com/shimmeringbee/da/capabilities" + "github.com/shimmeringbee/persistence/impl/memory" "github.com/shimmeringbee/zda/implcaps/generic" "github.com/shimmeringbee/zigbee" "github.com/stretchr/testify/assert" @@ -12,7 +13,7 @@ import ( func Test_gateway_createNode(t *testing.T) { t.Run("creates a new node if non exists", func(t *testing.T) { - g := New(context.Background(), nil, nil).(*gateway) + g := New(context.Background(), memory.New(), nil, nil).(*gateway) addr := zigbee.GenerateLocalAdministeredIEEEAddress() _, found := g.node[addr] @@ -29,7 +30,7 @@ func Test_gateway_createNode(t *testing.T) { }) t.Run("does not create a new node if already exists", func(t *testing.T) { - g := New(context.Background(), nil, nil).(*gateway) + g := New(context.Background(), memory.New(), nil, nil).(*gateway) addr := zigbee.GenerateLocalAdministeredIEEEAddress() n, _ := g.createNode(addr) @@ -43,7 +44,7 @@ func Test_gateway_createNode(t *testing.T) { func Test_gateway_getNode(t *testing.T) { t.Run("returns node if it is present", func(t *testing.T) { - g := New(context.Background(), nil, nil).(*gateway) + g := New(context.Background(), memory.New(), nil, nil).(*gateway) addr := zigbee.GenerateLocalAdministeredIEEEAddress() n, _ := g.createNode(addr) @@ -51,7 +52,7 @@ func Test_gateway_getNode(t *testing.T) { }) t.Run("returns nil if note is not present", func(t *testing.T) { - g := New(context.Background(), nil, nil).(*gateway) + g := New(context.Background(), memory.New(), nil, nil).(*gateway) addr := zigbee.GenerateLocalAdministeredIEEEAddress() assert.Nil(t, g.getNode(addr)) @@ -60,7 +61,7 @@ func Test_gateway_getNode(t *testing.T) { func Test_gateway_removeNode(t *testing.T) { t.Run("returns true and removes node if address is present", func(t *testing.T) { - g := New(context.Background(), nil, nil).(*gateway) + g := New(context.Background(), memory.New(), nil, nil).(*gateway) addr := zigbee.GenerateLocalAdministeredIEEEAddress() _, _ = g.createNode(addr) @@ -68,7 +69,7 @@ func Test_gateway_removeNode(t *testing.T) { }) t.Run("returns false if removing non existent address", func(t *testing.T) { - g := New(context.Background(), nil, nil).(*gateway) + g := New(context.Background(), memory.New(), nil, nil).(*gateway) addr := zigbee.GenerateLocalAdministeredIEEEAddress() assert.False(t, g.removeNode(addr)) @@ -77,12 +78,14 @@ func Test_gateway_removeNode(t *testing.T) { func Test_gateway_createNextDevice(t *testing.T) { t.Run("creates a new device on a node with the next free sub identifier", func(t *testing.T) { - g := New(context.Background(), nil, nil).(*gateway) + g := New(context.Background(), memory.New(), nil, nil).(*gateway) addr := zigbee.GenerateLocalAdministeredIEEEAddress() n, _ := g.createNode(addr) d := g.createNextDevice(n) + assert.Equal(t, n, d.n) + assert.Equal(t, addr, d.address.IEEEAddress) assert.Equal(t, uint8(0), d.address.SubIdentifier) assert.Equal(t, g, d.gw) @@ -111,7 +114,7 @@ func Test_gateway_createNextDevice(t *testing.T) { func Test_gateway_getDevice(t *testing.T) { t.Run("if a device is present it will be returned, and found will be true", func(t *testing.T) { - g := New(context.Background(), nil, nil).(*gateway) + g := New(context.Background(), memory.New(), nil, nil).(*gateway) addr := zigbee.GenerateLocalAdministeredIEEEAddress() n, _ := g.createNode(addr) @@ -122,7 +125,7 @@ func Test_gateway_getDevice(t *testing.T) { }) t.Run("if a device is missing nil will be returned, and found will be false", func(t *testing.T) { - g := New(context.Background(), nil, nil).(*gateway) + g := New(context.Background(), memory.New(), nil, nil).(*gateway) addr := zigbee.GenerateLocalAdministeredIEEEAddress() _, _ = g.createNode(addr) @@ -138,7 +141,7 @@ func Test_gateway_getDevice(t *testing.T) { func Test_gateway_getDevices(t *testing.T) { t.Run("returns all devices registered", func(t *testing.T) { - g := New(context.Background(), nil, nil).(*gateway) + g := New(context.Background(), memory.New(), nil, nil).(*gateway) addr1 := zigbee.GenerateLocalAdministeredIEEEAddress() n1, _ := g.createNode(addr1) d1 := g.createNextDevice(n1) @@ -156,7 +159,7 @@ func Test_gateway_getDevices(t *testing.T) { func Test_gateway_getDevicesOnNode(t *testing.T) { t.Run("returns all devices registered on the provided node", func(t *testing.T) { - g := New(context.Background(), nil, nil).(*gateway) + g := New(context.Background(), memory.New(), nil, nil).(*gateway) addr1 := zigbee.GenerateLocalAdministeredIEEEAddress() n1, _ := g.createNode(addr1) d1 := g.createNextDevice(n1) @@ -186,7 +189,7 @@ func drainEvents(g *gateway) []interface{} { func Test_gateway_removeDevice(t *testing.T) { t.Run("removes a device from a node, and returns true", func(t *testing.T) { - g := New(context.Background(), nil, nil).(*gateway) + g := New(context.Background(), memory.New(), nil, nil).(*gateway) addr := zigbee.GenerateLocalAdministeredIEEEAddress() n, _ := g.createNode(addr) @@ -207,7 +210,7 @@ func Test_gateway_removeDevice(t *testing.T) { }) t.Run("returns false if device can't be found on node", func(t *testing.T) { - g := New(context.Background(), nil, nil).(*gateway) + g := New(context.Background(), memory.New(), nil, nil).(*gateway) addr := zigbee.GenerateLocalAdministeredIEEEAddress() _, _ = g.createNode(addr) @@ -227,7 +230,7 @@ func Test_gateway_removeDevice(t *testing.T) { func Test_gateway_attachCapabilityToDevice(t *testing.T) { t.Run("attaches capability to device and emits event", func(t *testing.T) { - g := New(context.Background(), nil, nil).(*gateway) + g := New(context.Background(), memory.New(), nil, nil).(*gateway) addr := zigbee.GenerateLocalAdministeredIEEEAddress() n, _ := g.createNode(addr) @@ -243,12 +246,14 @@ func Test_gateway_attachCapabilityToDevice(t *testing.T) { events := drainEvents(g) assert.Len(t, events, 1) assert.IsType(t, da.CapabilityAdded{}, events[0]) + + assert.True(t, g.sectionForDevice(d.address).Section("capability").Exists(capabilities.StandardNames[capabilities.ProductInformationFlag])) }) } func Test_gateway_detachCapabilityFromDevice(t *testing.T) { t.Run("detaches a capability from device and emits event", func(t *testing.T) { - g := New(context.Background(), nil, nil).(*gateway) + g := New(context.Background(), memory.New(), nil, nil).(*gateway) addr := zigbee.GenerateLocalAdministeredIEEEAddress() n, _ := g.createNode(addr) @@ -257,6 +262,8 @@ func Test_gateway_detachCapabilityFromDevice(t *testing.T) { c := generic.NewProductInformation() g.attachCapabilityToDevice(d, c) + assert.True(t, g.sectionForDevice(d.address).Section("capability").Exists(capabilities.StandardNames[capabilities.ProductInformationFlag])) + _ = drainEvents(g) g.detachCapabilityFromDevice(d, c) @@ -266,10 +273,12 @@ func Test_gateway_detachCapabilityFromDevice(t *testing.T) { events := drainEvents(g) assert.Len(t, events, 1) assert.IsType(t, da.CapabilityRemoved{}, events[0]) + + assert.False(t, g.sectionForDevice(d.address).Section("capability").Exists(capabilities.StandardNames[capabilities.ProductInformationFlag])) }) t.Run("does nothing if called for unattached capability", func(t *testing.T) { - g := New(context.Background(), nil, nil).(*gateway) + g := New(context.Background(), memory.New(), nil, nil).(*gateway) addr := zigbee.GenerateLocalAdministeredIEEEAddress() n, _ := g.createNode(addr) diff --git a/utils.go b/utils.go index 29cac8a..0fd4bc2 100644 --- a/utils.go +++ b/utils.go @@ -1,6 +1,6 @@ package zda -func Contains[T comparable](haystack []T, needle T) bool { +func contains[T comparable](haystack []T, needle T) bool { for _, straw := range haystack { if straw == needle { return true