diff --git a/events.go b/Internal_events.go similarity index 100% rename from events.go rename to Internal_events.go diff --git a/da_events.go b/da_events.go new file mode 100644 index 0000000..de7560a --- /dev/null +++ b/da_events.go @@ -0,0 +1,17 @@ +package zda + +import "context" + +type eventSender interface { + sendEvent(event interface{}) +} + +func (g *gateway) sendEvent(event interface{}) { + //TODO implement me + panic("implement me") +} + +func (g *gateway) ReadEvent(_ context.Context) (interface{}, error) { + //TODO implement me + panic("implement me") +} diff --git a/da_events_test.go b/da_events_test.go new file mode 100644 index 0000000..bbf50b2 --- /dev/null +++ b/da_events_test.go @@ -0,0 +1,11 @@ +package zda + +import "github.com/stretchr/testify/mock" + +type mockEventSender struct { + mock.Mock +} + +func (m *mockEventSender) sendEvent(event interface{}) { + m.Called(event) +} diff --git a/device_discovery.go b/device_discovery.go new file mode 100644 index 0000000..8a6ce67 --- /dev/null +++ b/device_discovery.go @@ -0,0 +1,95 @@ +package zda + +import ( + "context" + "github.com/shimmeringbee/da" + "github.com/shimmeringbee/da/capabilities" + "github.com/shimmeringbee/logwrap" + "github.com/shimmeringbee/zigbee" + "time" +) + +type deviceDiscovery struct { + gateway da.Gateway + networkJoining zigbee.NetworkJoining + eventSender eventSender + + discovering bool + allowTimer *time.Timer + allowExpiresAt time.Time + + logger logwrap.Logger +} + +func (d *deviceDiscovery) Capability() da.Capability { + return capabilities.DeviceDiscoveryFlag +} + +func (d *deviceDiscovery) Name() string { + return capabilities.StandardNames[d.Capability()] +} + +func (d *deviceDiscovery) Enable(ctx context.Context, duration time.Duration) error { + d.logger.LogInfo(ctx, "Invoking PermitJoin on Zigbee provider.", logwrap.Datum("Duration", duration)) + if err := d.networkJoining.PermitJoin(ctx, true); err != nil { + d.logger.LogError(ctx, "Failed to PermitJoin on Zigbee provider.", logwrap.Err(err)) + return err + } + + if d.allowTimer != nil { + d.allowTimer.Stop() + } + + d.allowExpiresAt = time.Now().Add(duration) + d.allowTimer = time.AfterFunc(duration, func() { + cctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + defer cancel() + + if err := d.Disable(cctx); err != nil { + d.logger.LogError(cctx, "Automatic timed DenyJoin failed.", logwrap.Err(err)) + } + }) + + d.discovering = true + + d.eventSender.sendEvent(capabilities.DeviceDiscoveryEnabled{ + Gateway: d.gateway, + Duration: duration, + }) + return nil +} + +func (d *deviceDiscovery) Disable(ctx context.Context) error { + d.logger.LogInfo(ctx, "Invoking DenyJoin on Zigbee provider.") + if err := d.networkJoining.DenyJoin(ctx); err != nil { + d.logger.LogError(ctx, "Failed to DenyJoin on Zigbee provider.", logwrap.Err(err)) + return err + } + + d.discovering = false + d.allowTimer = nil + d.allowExpiresAt = time.Time{} + + d.eventSender.sendEvent(capabilities.DeviceDiscoveryDisabled{ + Gateway: d.gateway, + }) + return nil +} + +func (d *deviceDiscovery) Status(ctx context.Context) (capabilities.DeviceDiscoveryStatus, error) { + remainingDuration := d.allowExpiresAt.Sub(time.Now()) + if remainingDuration < 0 { + remainingDuration = 0 + } + + return capabilities.DeviceDiscoveryStatus{Discovering: d.discovering, RemainingDuration: remainingDuration}, nil +} + +func (d *deviceDiscovery) Stop() { + if d.allowTimer != nil { + d.allowTimer.Stop() + } +} + +var _ capabilities.DeviceDiscovery = (*deviceDiscovery)(nil) +var _ da.BasicCapability = (*deviceDiscovery)(nil) diff --git a/device_discovery_test.go b/device_discovery_test.go new file mode 100644 index 0000000..5d5d000 --- /dev/null +++ b/device_discovery_test.go @@ -0,0 +1,216 @@ +package zda + +import ( + "context" + "errors" + "github.com/shimmeringbee/da/capabilities" + "github.com/shimmeringbee/logwrap" + "github.com/shimmeringbee/logwrap/impl/discard" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "testing" + "time" +) + +type mockNetworkJoining struct { + mock.Mock +} + +func (m *mockNetworkJoining) PermitJoin(ctx context.Context, allRouters bool) error { + args := m.Called(ctx, allRouters) + return args.Error(0) +} + +func (m *mockNetworkJoining) DenyJoin(ctx context.Context) error { + args := m.Called(ctx) + return args.Error(0) +} + +func TestZigbeeDeviceDiscovery_Enable(t *testing.T) { + t.Run("calling enable on device which is self causes AllowJoin of zigbee provider", func(t *testing.T) { + mockEventSender := mockEventSender{} + mockEventSender.On("sendEvent", mock.IsType(capabilities.DeviceDiscoveryEnabled{})) + + mockNetworkJoining := mockNetworkJoining{} + mockNetworkJoining.On("PermitJoin", mock.Anything, true).Return(nil) + + zdd := deviceDiscovery{ + eventSender: &mockEventSender, + networkJoining: &mockNetworkJoining, + logger: logwrap.New(discard.Discard()), + } + defer zdd.Stop() + + err := zdd.Enable(context.Background(), 500*time.Millisecond) + assert.NoError(t, err) + + status, err := zdd.Status(context.Background()) + assert.NoError(t, err) + assert.True(t, status.Discovering) + + mockEventSender.AssertExpectations(t) + mockNetworkJoining.AssertExpectations(t) + }) + + t.Run("calling enable on device which is self causes AllowJoin of zigbee provider, and forwards an error", func(t *testing.T) { + mockEventSender := mockEventSender{} + + expectedError := errors.New("error") + mockNetworkJoining := mockNetworkJoining{} + mockNetworkJoining.On("PermitJoin", mock.Anything, true).Return(expectedError) + + zdd := deviceDiscovery{ + eventSender: &mockEventSender, + networkJoining: &mockNetworkJoining, + logger: logwrap.New(discard.Discard()), + } + defer zdd.Stop() + + err := zdd.Enable(context.Background(), 500*time.Millisecond) + assert.Error(t, err) + assert.Equal(t, expectedError, err) + + status, err := zdd.Status(context.Background()) + assert.NoError(t, err) + assert.False(t, status.Discovering) + + mockEventSender.AssertExpectations(t) + mockNetworkJoining.AssertExpectations(t) + }) +} + +func TestZigbeeDeviceDiscovery_Disable(t *testing.T) { + t.Run("calling disable on device which is self causes DenyJoin of zigbee provider", func(t *testing.T) { + mockEventSender := mockEventSender{} + mockEventSender.On("sendEvent", mock.IsType(capabilities.DeviceDiscoveryDisabled{})) + + mockNetworkJoining := mockNetworkJoining{} + mockNetworkJoining.On("DenyJoin", mock.Anything).Return(nil) + + zdd := deviceDiscovery{ + eventSender: &mockEventSender, + networkJoining: &mockNetworkJoining, + logger: logwrap.New(discard.Discard()), + } + defer zdd.Stop() + + zdd.discovering = true + + err := zdd.Disable(context.Background()) + assert.NoError(t, err) + + status, err := zdd.Status(context.Background()) + assert.NoError(t, err) + assert.False(t, status.Discovering) + + mockEventSender.AssertExpectations(t) + mockNetworkJoining.AssertExpectations(t) + }) + + t.Run("calling disable on device which is self causes DenyJoin of zigbee provider, and forwards an error", func(t *testing.T) { + expectedError := errors.New("deny join failure") + + mockEventSender := mockEventSender{} + + mockNetworkJoining := mockNetworkJoining{} + mockNetworkJoining.On("DenyJoin", mock.Anything).Return(expectedError) + + zdd := deviceDiscovery{ + eventSender: &mockEventSender, + networkJoining: &mockNetworkJoining, + logger: logwrap.New(discard.Discard()), + } + defer zdd.Stop() + + zdd.discovering = true + + err := zdd.Disable(context.Background()) + + assert.Error(t, err) + assert.Equal(t, expectedError, err) + + status, err := zdd.Status(context.Background()) + assert.NoError(t, err) + assert.True(t, status.Discovering) + + mockEventSender.AssertExpectations(t) + mockNetworkJoining.AssertExpectations(t) + }) +} + +func TestZigbeeDeviceDiscovery_DurationBehaviour(t *testing.T) { + t.Run("when an allows duration expires then a disable instruction is sent", func(t *testing.T) { + mockEventSender := mockEventSender{} + mockEventSender.On("sendEvent", mock.Anything).Return(nil).Twice() + + mockNetworkJoining := mockNetworkJoining{} + mockNetworkJoining.On("PermitJoin", mock.Anything, true).Return(nil) + mockNetworkJoining.On("DenyJoin", mock.Anything).Return(nil) + + zdd := deviceDiscovery{ + eventSender: &mockEventSender, + networkJoining: &mockNetworkJoining, + logger: logwrap.New(discard.Discard()), + } + + defer zdd.Stop() + + err := zdd.Enable(context.Background(), 100*time.Millisecond) + assert.NoError(t, err) + + status, err := zdd.Status(context.Background()) + assert.NoError(t, err) + assert.True(t, status.Discovering) + + time.Sleep(150 * time.Millisecond) + + status, err = zdd.Status(context.Background()) + assert.NoError(t, err) + assert.False(t, status.Discovering) + + mockEventSender.AssertExpectations(t) + mockNetworkJoining.AssertExpectations(t) + }) + + t.Run("second allows extend the duration of the first", func(t *testing.T) { + mockEventSender := mockEventSender{} + mockEventSender.On("sendEvent", mock.Anything).Return(nil).Twice() + + mockNetworkJoining := mockNetworkJoining{} + mockNetworkJoining.On("PermitJoin", mock.Anything, true).Return(nil) + mockNetworkJoining.On("DenyJoin", mock.Anything).Return(nil).Maybe() + + zdd := deviceDiscovery{ + eventSender: &mockEventSender, + networkJoining: &mockNetworkJoining, + logger: logwrap.New(discard.Discard()), + } + + defer zdd.Stop() + + err := zdd.Enable(context.Background(), 50*time.Millisecond) + assert.NoError(t, err) + + status, err := zdd.Status(context.Background()) + assert.NoError(t, err) + assert.True(t, status.Discovering) + assert.Greater(t, int64(status.RemainingDuration), int64(45*time.Millisecond)) + + err = zdd.Enable(context.Background(), 200*time.Millisecond) + assert.NoError(t, err) + + status, err = zdd.Status(context.Background()) + assert.NoError(t, err) + assert.True(t, status.Discovering) + assert.Greater(t, int64(status.RemainingDuration), int64(145*time.Millisecond)) + + time.Sleep(150 * time.Millisecond) + + status, err = zdd.Status(context.Background()) + assert.NoError(t, err) + assert.True(t, status.Discovering) + + mockEventSender.AssertExpectations(t) + mockNetworkJoining.AssertExpectations(t) + }) +} diff --git a/gateway.go b/gateway.go index d68c91d..5e0d690 100644 --- a/gateway.go +++ b/gateway.go @@ -55,11 +55,6 @@ type gateway struct { ruleExecutor ruleExecutor } -func (g *gateway) ReadEvent(_ context.Context) (interface{}, error) { - //TODO implement me - panic("implement me") -} - func (g *gateway) Capabilities() []da.Capability { //TODO implement me panic("implement me") @@ -87,6 +82,12 @@ func (g *gateway) Start(ctx context.Context) error { g.selfDevice = gatewayDevice{ gateway: g, identifier: adapterNode.IEEEAddress, + dd: &deviceDiscovery{ + gateway: g, + networkJoining: g.provider, + eventSender: g, + logger: g.logger, + }, } g.logger.LogInfo(g.ctx, "Adapter coordinator IEEE address.", logwrap.Datum("IEEEAddress", g.selfDevice.Identifier().String())) @@ -103,6 +104,7 @@ func (g *gateway) Start(ctx context.Context) error { func (g *gateway) Stop(_ context.Context) error { g.logger.LogInfo(g.ctx, "Stopping ZDA.") + g.selfDevice.dd.Stop() g.ctxCancel() return nil } @@ -112,6 +114,7 @@ var _ da.Gateway = (*gateway)(nil) type gatewayDevice struct { gateway da.Gateway identifier da.Identifier + dd *deviceDiscovery } func (g gatewayDevice) Gateway() da.Gateway { @@ -127,8 +130,12 @@ func (g gatewayDevice) Capabilities() []da.Capability { } func (g gatewayDevice) Capability(capability da.Capability) da.BasicCapability { - //TODO implement me - panic("implement me") + switch capability { + case capabilities.DeviceDiscoveryFlag: + return g.dd + default: + return nil + } } var _ da.Device = (*gatewayDevice)(nil)