Skip to content

Commit

Permalink
Add workaround for tirouters.
Browse files Browse the repository at this point in the history
  • Loading branch information
pwood committed Jun 21, 2024
1 parent 9e2582f commit b98e015
Show file tree
Hide file tree
Showing 9 changed files with 289 additions and 12 deletions.
12 changes: 8 additions & 4 deletions attribute/monitor.go
Original file line number Diff line number Diff line change
Expand Up @@ -178,15 +178,17 @@ func (z *zclMonitor) Attach(ctx context.Context, e zigbee.Endpoint, c zigbee.Clu
converter.Store(z.config, AttributeIdKey, z.attributeID, converter.AttributeIDEncoder)
converter.Store(z.config, AttributeDataTypeKey, z.attributeDataType, converter.AttributeDataTypeEncoder)

var reportingErr error

if rc.Mode == AttemptConfigureReporting {
z.logger.Info(ctx, "Attempting to configure attribute reporting.")

if err := z.nodeBinder.BindNodeToController(ctx, z.ieeeAddress, z.localEndpoint, e, c); err != nil {
z.logger.Warn(ctx, "Binding node to controller failed.", logwrap.Err(err))
if reportingErr = z.nodeBinder.BindNodeToController(ctx, z.ieeeAddress, z.localEndpoint, e, c); reportingErr != nil {
z.logger.Warn(ctx, "Binding node to controller failed.", logwrap.Err(reportingErr))
failedReporting = true
} else {
if err := z.zclCommunicator.ConfigureReporting(ctx, z.ieeeAddress, ack, z.clusterID, zigbee.NoManufacturer, z.localEndpoint, z.remoteEndpoint, seq, z.attributeID, z.attributeDataType, uint16(math.Round(rc.MinimumInterval.Seconds())), uint16(math.Round(rc.MaximumInterval.Seconds())), rc.ReportableChange); err != nil {
z.logger.Warn(ctx, "Configure reporting failed.", logwrap.Err(err))
if reportingErr = z.zclCommunicator.ConfigureReporting(ctx, z.ieeeAddress, ack, z.clusterID, zigbee.NoManufacturer, z.localEndpoint, z.remoteEndpoint, seq, z.attributeID, z.attributeDataType, uint16(math.Round(rc.MinimumInterval.Seconds())), uint16(math.Round(rc.MaximumInterval.Seconds())), rc.ReportableChange); reportingErr != nil {
z.logger.Warn(ctx, "Configure reporting failed.", logwrap.Err(reportingErr))
failedReporting = true
} else {
z.config.Set(ReportingConfiguredKey, true)
Expand All @@ -198,6 +200,8 @@ func (z *zclMonitor) Attach(ctx context.Context, e zigbee.Endpoint, c zigbee.Clu
if (failedReporting && pc.Mode == PollIfReportingFailed) || pc.Mode == AlwaysPoll {
z.config.Set(PollingConfiguredKey, true)
converter.Store(z.config, PollingIntervalKey, pc.Interval, converter.DurationEncoder)
} else if failedReporting && pc.Mode == NeverPoll {
return fmt.Errorf("reporting failed, never poll, attach failed: %w", reportingErr)
}

return z.reattach(ctx)
Expand Down
30 changes: 30 additions & 0 deletions attribute/monitor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,36 @@ func Test_zclMonitor_Attach(t *testing.T) {
assert.NotNil(t, z.match)
})

t.Run("attach fails for reporting only failed binding", func(t *testing.T) {
mzc := &mocks.MockZCLCommunicator{}
defer mzc.AssertExpectations(t)

mzp := &zigbee.MockProvider{}
defer mzp.AssertExpectations(t)
expectedIeee := zigbee.GenerateLocalAdministeredIEEEAddress()

mzp.On("BindNodeToController", mock.Anything, expectedIeee, zigbee.Endpoint(2), zigbee.Endpoint(1), zigbee.ClusterID(2)).Return(io.EOF)

s := memory.New()

d := &mocks2.MockDevice{}
defer d.AssertExpectations(t)
d.On("Identifier").Return(zigbee.GenerateLocalAdministeredIEEEAddress())

cb := func(zcl.AttributeID, zcl.AttributeDataTypeValue) {}

tl := func(dd da.Device, _ zigbee.ProfileID) (zigbee.IEEEAddress, zigbee.Endpoint, bool, uint8) {
assert.Equal(t, d, dd)
return expectedIeee, 2, false, 0
}

z := NewMonitor(mzc, mzp, tl, logwrap.New(discard.Discard())).(*zclMonitor)
z.Init(s, d, cb)

err := z.Attach(context.Background(), 1, 2, 3, zcl.TypeUnsignedInt8, ReportingConfig{Mode: AttemptConfigureReporting, MinimumInterval: 1 * time.Minute, MaximumInterval: 5 * time.Minute, ReportableChange: nil}, PollingConfig{Mode: NeverPoll})
assert.Error(t, err)
})

t.Run("attach succeeds for reporting only", func(t *testing.T) {
mzc := &mocks.MockZCLCommunicator{}
defer mzc.AssertExpectations(t)
Expand Down
3 changes: 2 additions & 1 deletion enumerate_device.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,8 @@ func (e enumerateDevice) startEnumeration(ctx context.Context, n *node) error {
return fmt.Errorf("enumeration already in progress")
}

go e.enumerate(ctx, n)
newCtx := context.WithoutCancel(ctx)
go e.enumerate(newCtx, n)

return nil
}
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -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-20240615193906-993b4195d59e
github.com/shimmeringbee/da v0.0.0-20240621202518-c4ba1b61593d
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
Expand Down
6 changes: 6 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,12 @@ github.com/shimmeringbee/da v0.0.0-20240614104703-d8efc964b76a h1:gqBGoPcDjCzttr
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/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=
Expand Down
17 changes: 11 additions & 6 deletions implcaps/factory/mapping.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"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/zcl/humidity_sensor"
"github.com/shimmeringbee/zda/implcaps/zcl/identify"
"github.com/shimmeringbee/zda/implcaps/zcl/power_supply"
Expand All @@ -18,14 +19,16 @@ const ZCLHumiditySensor = "ZCLHumiditySensor"
const ZCLPressureSensor = "ZCLPressureSensor"
const ZCLIdentify = "ZCLIdentify"
const ZCLPowerSupply = "ZCLPowerSupply"
const ProprietaryTiRouterDeviceWorkaround = "ProprietaryTiRouterDeviceWorkaround"

var Mapping = map[string]da.Capability{
GenericProductInformation: capabilities.ProductInformationFlag,
ZCLTemperatureSensor: capabilities.TemperatureSensorFlag,
ZCLHumiditySensor: capabilities.RelativeHumiditySensorFlag,
ZCLPressureSensor: capabilities.PressureSensorFlag,
ZCLIdentify: capabilities.IdentifyFlag,
ZCLPowerSupply: capabilities.PowerSupplyFlag,
GenericProductInformation: capabilities.ProductInformationFlag,
ZCLTemperatureSensor: capabilities.TemperatureSensorFlag,
ZCLHumiditySensor: capabilities.RelativeHumiditySensorFlag,
ZCLPressureSensor: capabilities.PressureSensorFlag,
ZCLIdentify: capabilities.IdentifyFlag,
ZCLPowerSupply: capabilities.PowerSupplyFlag,
ProprietaryTiRouterDeviceWorkaround: capabilities.DeviceWorkaroundFlag,
}

func Create(name string, iface implcaps.ZDAInterface) implcaps.ZDACapability {
Expand All @@ -42,6 +45,8 @@ func Create(name string, iface implcaps.ZDAInterface) implcaps.ZDACapability {
return identify.NewIdentify(iface)
case ZCLPowerSupply:
return power_suply.NewPowerSupply(iface)
case ProprietaryTiRouterDeviceWorkaround:
return device_workaround.NewDeviceWorkaround(iface)
default:
return nil
}
Expand Down
88 changes: 88 additions & 0 deletions implcaps/proprietary/tirouter/device_workaround/impl.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,88 @@
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) {

}
125 changes: 125 additions & 0 deletions implcaps/proprietary/tirouter/device_workaround/impl_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,125 @@
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)
})
}
18 changes: 18 additions & 0 deletions rules/proprietary.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"Name": "proprietary",
"Rules": [
{
"Description": "TI Routers",
"Filter": "Product[Self].Name == 'ti.router'",
"Actions": {
"Capabilities": {
"Add": {
"ProprietaryTiRouterDeviceWorkaround": {
"ZigbeeEndpoint": "Fn.Endpoint(Self)"
}
}
}
}
}
]
}

0 comments on commit b98e015

Please sign in to comment.