-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Implemented bthome bluetooth protocol
- Loading branch information
Showing
11 changed files
with
384 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
package shelly | ||
|
||
import ( | ||
"context" | ||
|
||
"github.com/nikiforov-soft/yasp/config" | ||
"github.com/nikiforov-soft/yasp/device" | ||
) | ||
|
||
// https://shelly-api-docs.shelly.cloud/docs-ble/Devices/button/ | ||
func init() { | ||
err := device.RegisterDevice("SBBT-002C", func(ctx context.Context, config *config.Device) (device.Device, error) { | ||
return &shellyBtDevice{ | ||
name: config.Name, | ||
deviceType: config.Type, | ||
}, nil | ||
}) | ||
if err != nil { | ||
panic(err) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
package shelly | ||
|
||
import ( | ||
"context" | ||
|
||
"github.com/nikiforov-soft/yasp/config" | ||
"github.com/nikiforov-soft/yasp/device" | ||
) | ||
|
||
// https://shelly-api-docs.shelly.cloud/docs-ble/Devices/wall_eu | ||
func init() { | ||
err := device.RegisterDevice("SBBT-004CEU", func(ctx context.Context, config *config.Device) (device.Device, error) { | ||
return &shellyBtDevice{ | ||
name: config.Name, | ||
deviceType: config.Type, | ||
}, nil | ||
}) | ||
if err != nil { | ||
panic(err) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
package shelly | ||
|
||
import ( | ||
"context" | ||
|
||
"github.com/nikiforov-soft/yasp/config" | ||
"github.com/nikiforov-soft/yasp/device" | ||
) | ||
|
||
// https://shelly-api-docs.shelly.cloud/docs-ble/Devices/wall_us | ||
func init() { | ||
err := device.RegisterDevice("SBBT-004CUS", func(ctx context.Context, config *config.Device) (device.Device, error) { | ||
return &shellyBtDevice{ | ||
name: config.Name, | ||
deviceType: config.Type, | ||
}, nil | ||
}) | ||
if err != nil { | ||
panic(err) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
package shelly | ||
|
||
import ( | ||
"context" | ||
|
||
"github.com/nikiforov-soft/yasp/config" | ||
"github.com/nikiforov-soft/yasp/device" | ||
) | ||
|
||
// https://shelly-api-docs.shelly.cloud/docs-ble/Devices/dw/ | ||
func init() { | ||
err := device.RegisterDevice("SBDW-002C", func(ctx context.Context, config *config.Device) (device.Device, error) { | ||
return &shellyBtDevice{ | ||
name: config.Name, | ||
deviceType: config.Type, | ||
}, nil | ||
}) | ||
if err != nil { | ||
panic(err) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
package shelly | ||
|
||
import ( | ||
"context" | ||
|
||
"github.com/nikiforov-soft/yasp/config" | ||
"github.com/nikiforov-soft/yasp/device" | ||
) | ||
|
||
// https://shelly-api-docs.shelly.cloud/docs-ble/Devices/ht/ | ||
func init() { | ||
err := device.RegisterDevice("SBHT-003C", func(ctx context.Context, config *config.Device) (device.Device, error) { | ||
return &shellyBtDevice{ | ||
name: config.Name, | ||
deviceType: config.Type, | ||
}, nil | ||
}) | ||
if err != nil { | ||
panic(err) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
package shelly | ||
|
||
import ( | ||
"context" | ||
|
||
"github.com/nikiforov-soft/yasp/config" | ||
"github.com/nikiforov-soft/yasp/device" | ||
) | ||
|
||
// https://shelly-api-docs.shelly.cloud/docs-ble/Devices/motion | ||
func init() { | ||
err := device.RegisterDevice("SBMO-003Z", func(ctx context.Context, config *config.Device) (device.Device, error) { | ||
return &shellyBtDevice{ | ||
name: config.Name, | ||
deviceType: config.Type, | ||
}, nil | ||
}) | ||
if err != nil { | ||
panic(err) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
package shelly | ||
|
||
import ( | ||
"context" | ||
"encoding/hex" | ||
"encoding/json" | ||
"fmt" | ||
|
||
"github.com/nikiforov-soft/yasp/device" | ||
"github.com/nikiforov-soft/yasp/device/vendors/bthome" | ||
) | ||
|
||
type shellyBtDevice struct { | ||
name string | ||
deviceType string | ||
} | ||
|
||
func (sbd *shellyBtDevice) Decode(_ context.Context, data *device.Data) (*device.Data, error) { | ||
sensorData, err := bthome.Parse(data.Data) | ||
if err != nil { | ||
return nil, fmt.Errorf("shelly: failed to parse sensor data: %s - %w", hex.EncodeToString(data.Data), err) | ||
} | ||
|
||
result, err := json.Marshal(sensorData) | ||
if err != nil { | ||
return nil, fmt.Errorf("shelly: failed to marshal sensor data: %w", err) | ||
} | ||
|
||
properties := make(map[string]interface{}, len(data.Properties)+8) | ||
for k, v := range data.Properties { | ||
properties[k] = v | ||
} | ||
properties["deviceName"] = sbd.name | ||
properties["deviceType"] = sbd.deviceType | ||
properties["batteryPercent"] = sensorData.BatteryPercent | ||
properties["illuminanceLux"] = sensorData.IlluminanceLux | ||
properties["motionState"] = sensorData.MotionState | ||
properties["windowState"] = sensorData.WindowState | ||
properties["humidityPercent"] = sensorData.HumidityPercent | ||
properties["buttonEvent"] = sensorData.ButtonEvent | ||
properties["rotationDegrees"] = sensorData.RotationDegrees | ||
properties["temperatureCelsius"] = sensorData.TemperatureCelsius | ||
|
||
return &device.Data{ | ||
Data: result, | ||
Properties: properties, | ||
}, nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,124 @@ | ||
package bthome | ||
|
||
import ( | ||
"bytes" | ||
"encoding/binary" | ||
"errors" | ||
"fmt" | ||
"io" | ||
) | ||
|
||
// Parse - Parses bthome encoded data | ||
// https://bthome.io/format/#sensor-data | ||
func Parse(data []byte) (*SensorData, error) { | ||
if len(data) < 3 { | ||
return nil, fmt.Errorf("bthome: invalid data length: %d", len(data)) | ||
} | ||
|
||
r := bytes.NewReader(data) | ||
header, err := r.ReadByte() | ||
if err != nil { | ||
return nil, fmt.Errorf("bthome: failed to read header id: %w", err) | ||
} | ||
if header != 0x44 { | ||
return nil, fmt.Errorf("bthome: invalid header type: %d", header) | ||
} | ||
|
||
var sensorData SensorData | ||
for { | ||
propertyType, err := r.ReadByte() | ||
if err != nil { | ||
if errors.Is(err, io.EOF) { | ||
break | ||
} | ||
return nil, fmt.Errorf("bthome: failed to read property type: %d", propertyType) | ||
} | ||
|
||
switch propertyType { | ||
case 0x00: // packet id | ||
/** | ||
https://bthome.io/format/#misc-data | ||
* The packet id is optional and can be used to filter duplicate data. | ||
* This allows you to send multiple advertisements that are exactly the same, to improve the chance that the advertisement arrives. | ||
* BTHome receivers should only process the advertisement if the packet id is different compared to the previous one. | ||
* The packet id is a value between 0 (0x00) and 255 (0xFF), and should be increased on every change in data. | ||
* Note that most home automation software already have some other filtering for unchanged data. | ||
* The use of a packet id is therefore often not needed. | ||
**/ | ||
packetId, err := r.ReadByte() | ||
if err != nil { | ||
return nil, err | ||
} | ||
sensorData.PacketId = packetId | ||
case 0x01: // battery % | ||
battery, err := r.ReadByte() | ||
if err != nil { | ||
return nil, fmt.Errorf("bthome: failed to read battery: %w", err) | ||
} | ||
sensorData.BatteryPercent = battery | ||
case 0x05: // illuminance lux | ||
illuminance, err := readIlluminance(r) | ||
if err != nil { | ||
return nil, fmt.Errorf("bthome: failed to read illuminance: %w", err) | ||
} | ||
sensorData.IlluminanceLux = illuminance | ||
case 0x21: // motion state | ||
motion, err := r.ReadByte() | ||
if err != nil { | ||
return nil, fmt.Errorf("bthome: failed to read motion state: %w", err) | ||
} | ||
sensorData.MotionState = motion | ||
case 0x2D: // window state | ||
windowState, err := r.ReadByte() | ||
if err != nil { | ||
return nil, fmt.Errorf("bthome: failed to read window state: %w", err) | ||
} | ||
sensorData.WindowState = windowState | ||
case 0x2E: // humidity % | ||
humidity, err := r.ReadByte() | ||
if err != nil { | ||
return nil, fmt.Errorf("bthome: failed to read humidity: %w", err) | ||
} | ||
sensorData.HumidityPercent = humidity | ||
case 0x3A: // button | ||
var buttonEvent uint16 | ||
if err := binary.Read(r, binary.BigEndian, &buttonEvent); err != nil { | ||
return nil, fmt.Errorf("bthome: failed to read button event: %w", err) | ||
} | ||
sensorData.ButtonEvent = buttonEvent | ||
case 0x3F: // rotation ° | ||
var rotation int16 | ||
if err := binary.Read(r, binary.BigEndian, &rotation); err != nil { | ||
return nil, fmt.Errorf("bthome: failed to read rotation: %w", err) | ||
} | ||
sensorData.RotationDegrees = float32(rotation) / 10. | ||
case 0x45: // temperature °C | ||
var temperature int16 | ||
if err := binary.Read(r, binary.BigEndian, &temperature); err != nil { | ||
return nil, fmt.Errorf("bthome: failed to read temperature: %w", err) | ||
} | ||
sensorData.TemperatureCelsius = float32(temperature) / 10. | ||
default: | ||
return nil, fmt.Errorf("bthome: unknown property type: %d", propertyType) | ||
} | ||
} | ||
|
||
return &sensorData, nil | ||
} | ||
|
||
func readIlluminance(r io.Reader) (float32, error) { | ||
var component12 uint16 | ||
err := binary.Read(r, binary.BigEndian, &component12) | ||
if err != nil { | ||
return 0, err | ||
} | ||
|
||
var component3 byte | ||
err = binary.Read(r, binary.BigEndian, &component3) | ||
if err != nil { | ||
return 0, err | ||
} | ||
|
||
illuminance := uint32(component12)<<8 | uint32(component3) | ||
return float32(illuminance) / 100., nil | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,72 @@ | ||
package bthome | ||
|
||
import ( | ||
"testing" | ||
|
||
"github.com/stretchr/testify/assert" | ||
) | ||
|
||
func TestParse(t *testing.T) { | ||
tests := []struct { | ||
name string | ||
data []byte | ||
expected *SensorData | ||
expectedError string | ||
}{ | ||
{ | ||
name: "invalid length", | ||
data: []byte{0x00}, | ||
expected: nil, | ||
expectedError: "bthome: invalid data length: 1", | ||
}, | ||
{ | ||
name: "invalid header", | ||
data: []byte{0x00, 0x00, 0x00}, | ||
expected: nil, | ||
expectedError: "bthome: invalid header type: 0", | ||
}, | ||
{ | ||
name: "invalid property", | ||
data: []byte{0x44, 0x00, 0x00, 0x98, 0x00}, | ||
expected: nil, | ||
expectedError: "bthome: unknown property type: 152", | ||
}, | ||
{ | ||
name: "packet id 43, battery 100%, illuminance 144192.00 lux, window 0, button event 0, rotation 0.0", | ||
data: []byte{0x44, 0x00, 0x2b, 0x01, 0x64, 0x05, 0xdc, 0x05, 0x00, 0x2d, 0x00, 0x3f, 0x00, 0x00}, | ||
expected: &SensorData{ | ||
PacketId: 43, | ||
BatteryPercent: 100, | ||
IlluminanceLux: 144192.00, | ||
WindowState: 0, | ||
ButtonEvent: 0, | ||
RotationDegrees: 0, | ||
}, | ||
expectedError: "", | ||
}, | ||
{ | ||
name: "packet id 44, battery 100%, illuminance 13120.00 lux, window 1, button event 0, rotation 0.0", | ||
data: []byte{0x44, 0x00, 0x2c, 0x01, 0x64, 0x05, 0x14, 0x05, 0x00, 0x2d, 0x01, 0x3f, 0x00, 0x00}, | ||
expected: &SensorData{ | ||
PacketId: 44, | ||
BatteryPercent: 100, | ||
IlluminanceLux: 13120.00, | ||
WindowState: 1, | ||
ButtonEvent: 0, | ||
RotationDegrees: 0, | ||
}, | ||
expectedError: "", | ||
}, | ||
} | ||
for _, test := range tests { | ||
t.Run(test.name, func(t *testing.T) { | ||
actual, err := Parse(test.data) | ||
if test.expectedError != "" { | ||
assert.EqualError(t, err, test.expectedError) | ||
} else { | ||
assert.NoError(t, err) | ||
assert.Equal(t, test.expected, actual) | ||
} | ||
}) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
package bthome | ||
|
||
type SensorData struct { | ||
PacketId byte `json:"packetId,omitempty"` | ||
BatteryPercent byte `json:"batteryPercent,omitempty"` | ||
IlluminanceLux float32 `json:"illuminanceLux,omitempty"` | ||
MotionState byte `json:"motionState,omitempty"` | ||
WindowState byte `json:"windowState,omitempty"` | ||
HumidityPercent byte `json:"humidityPercent,omitempty"` | ||
ButtonEvent uint16 `json:"buttonEvent,omitempty"` | ||
RotationDegrees float32 `json:"rotationDegrees,omitempty"` | ||
TemperatureCelsius float32 `json:"temperatureCelsius,omitempty"` | ||
} |