Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions custom_components/ble_monitor/ble_parser/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
from .sensorpush import parse_sensorpush
from .senssun import parse_senssun
from .smartdry import parse_smartdry
from .sonoff import parse_sonoff
from .switchbot import parse_switchbot
from .teltonika import parse_teltonika
from .thermobeacon import parse_thermobeacon
Expand Down Expand Up @@ -136,6 +137,11 @@ def parse_raw_data(self, data):
elif adstuct_type == 0x03:
# AD type 'Complete List of 16-bit Service Class UUIDs'
service_class_uuid16 = (adstruct[2] << 8) | adstruct[3]
elif adstuct_type == 0x05:
# AD type 'Complete List of 32-bit Service Class UUIDs'
if mac == b"\x66\x55\x44\x33\x22\x11":
# Sonoff specific data
man_spec_data_list.append(adstruct)
elif adstuct_type == 0x06:
# AD type '128-bit Service Class UUIDs'
service_class_uuid128 = adstruct[2:]
Expand Down Expand Up @@ -429,6 +435,10 @@ def parse_advertisement(
# Grundfos
sensor_data = parse_grundfos(self, man_spec_data, mac)
break
elif comp_id == 0xFFFF and man_spec_data[4:6] == b"\xee\x1b" and mac == b"\x66\x55\x44\x33\x22\x11":
# Sonoff
sensor_data = parse_sonoff(self, man_spec_data, mac)
break
elif comp_id == 0xFFFF and data_len == 0x1E:
# Kegtron
sensor_data = parse_kegtron(self, man_spec_data, mac)
Expand Down
127 changes: 127 additions & 0 deletions custom_components/ble_monitor/ble_parser/sonoff.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
"""Parser for Sonoff BLE advertisements"""
import logging
from typing import Any

from .helpers import to_mac, to_unformatted_mac

_LOGGER = logging.getLogger(__name__)

SONOFF_MODEL_MAP = {
0x46: "S-MATE",
0x47: "R5"
}

SONOFF_BUTTON_MAP = {
"S-MATE": {
0x00: "three btn switch left",
0x01: "three btn switch middle",
0x02: "three btn switch right"
},
"R5": {
0x00: "six btn switch top left",
0x01: "six btn switch top middle",
0x02: "six btn switch top right",
0x03: "six btn switch bottom left",
0x04: "six btn switch bottom middle",
0x05: "six btn switch bottom right"
}
}

SONOFF_ACTION_MAP = {
0x00: "single press",
0x01: "double press",
0x02: "long press"
}


def decrypt_sonoff(encrypted_data: bytes, seed: int) -> bytes:
xor_table = [0x0f, 0x39, 0xbe, 0x5f, 0x27, 0x05, 0xbe, 0xf9, 0x66, 0xb5,
0x74, 0x0d, 0x04, 0x86, 0xd2, 0x61, 0x55, 0xbb, 0xfc, 0x16,
0x34, 0x40, 0x7e, 0x1d, 0x38, 0x6e, 0xe4, 0x06, 0xaa, 0x79,
0x32, 0x95, 0x66, 0xb5, 0x74, 0x0d, 0xdb, 0x8c, 0xe9, 0x01,
0x2a]
xor_table_len = len(xor_table)

decrypted_data = bytearray()

for i, b in enumerate(encrypted_data):
decrypted_data.append(b ^ seed ^ xor_table[i % xor_table_len])

return bytes(decrypted_data)


def parse_sonoff(self, data: bytes, mac: bytes) -> dict[str, Any] | None:
# Verify MAC address and data length
if mac != b"\x66\x55\x44\x33\x22\x11" or len(data) < 10:
return None

firmware = "Sonoff"

# data_uuid32 = data[2:6]
# data_magic_1 = data[6:10]
data_device_type = data[10]
# data_magic_2 = data[11]
# data_sequence_number = data[12]
data_device_id = data[13:17]
data_seed = data[17]
data_encrypted = data[18:]

data_decrypted = decrypt_sonoff(data_encrypted, data_seed)

# data_padding_1 = data_decrypted[0]
data_button_id = data_decrypted[1]
data_press_type = data_decrypted[2]
data_press_counter = data_decrypted[3:7]
# data_padding_2 = data_decrypted[7]
# data_crc = data_decrypted[8:10]

unique_mac = b"\x00\x00" + data_device_id

# Check for duplicate messages
packet_id = int.from_bytes(data_press_counter, "big")
try:
prev_packet = self.lpacket_ids[unique_mac]
except KeyError:
# start with empty first packet
prev_packet = None
if prev_packet == packet_id:
# only process new messages
if self.filter_duplicates is True:
return None
self.lpacket_ids[unique_mac] = packet_id

try:
device_type = SONOFF_MODEL_MAP[data_device_type]
except KeyError:
if self.report_unknown == "Sonoff":
_LOGGER.info(
"BLE ADV from UNKNOWN Sonoff DEVICE: MAC: %s, ADV: %s",
to_mac(mac),
data.hex()
)
return None

try:
result = {
SONOFF_BUTTON_MAP[device_type][data_button_id]: "toggle",
"button switch": SONOFF_ACTION_MAP[data_press_type]
}
except KeyError:
_LOGGER.error(
"Unknown button id (%s) or press type (%s) from Sonoff DEVICE: MAC: %s, ADV: %s",
hex(data_button_id),
hex(data_press_type),
to_mac(mac),
data.hex()
)
return None

result.update({
"mac": to_unformatted_mac(unique_mac),
"type": device_type,
"packet": packet_id,
"firmware": firmware,
"data": True
})

return result
71 changes: 71 additions & 0 deletions custom_components/ble_monitor/const.py
Original file line number Diff line number Diff line change
Expand Up @@ -1917,6 +1917,72 @@ class BLEMonitorBinarySensorEntityDescription(
device_class=None,
state_class=None,
),
BLEMonitorSensorEntityDescription(
key="six btn switch top left",
sensor_class="SwitchSensor",
update_behavior="Instantly",
name="ble six button switch top left",
unique_id="top_left_switch_",
icon="mdi:gesture-tap-button",
native_unit_of_measurement=None,
device_class=None,
state_class=None,
),
BLEMonitorSensorEntityDescription(
key="six btn switch top middle",
sensor_class="SwitchSensor",
update_behavior="Instantly",
name="ble six button switch top middle",
unique_id="top_middle_switch_",
icon="mdi:gesture-tap-button",
native_unit_of_measurement=None,
device_class=None,
state_class=None,
),
BLEMonitorSensorEntityDescription(
key="six btn switch top right",
sensor_class="SwitchSensor",
update_behavior="Instantly",
name="ble six button switch top right",
unique_id="top_right_switch_",
icon="mdi:gesture-tap-button",
native_unit_of_measurement=None,
device_class=None,
state_class=None,
),
BLEMonitorSensorEntityDescription(
key="six btn switch bottom left",
sensor_class="SwitchSensor",
update_behavior="Instantly",
name="ble six button switch bottom left",
unique_id="bottom_left_switch_",
icon="mdi:gesture-tap-button",
native_unit_of_measurement=None,
device_class=None,
state_class=None,
),
BLEMonitorSensorEntityDescription(
key="six btn switch bottom middle",
sensor_class="SwitchSensor",
update_behavior="Instantly",
name="ble six button switch bottom middle",
unique_id="bottom_middle_switch_",
icon="mdi:gesture-tap-button",
native_unit_of_measurement=None,
device_class=None,
state_class=None,
),
BLEMonitorSensorEntityDescription(
key="six btn switch bottom right",
sensor_class="SwitchSensor",
update_behavior="Instantly",
name="ble six button switch bottom right",
unique_id="bottom_right_switch_",
icon="mdi:gesture-tap-button",
native_unit_of_measurement=None,
device_class=None,
state_class=None,
),
BLEMonitorSensorEntityDescription(
key="remote",
sensor_class="BaseRemoteSensor",
Expand Down Expand Up @@ -2138,6 +2204,8 @@ class BLEMonitorBinarySensorEntityDescription(
'ST10' : [["temperature", "battery", "rssi"], [], []],
'MS1' : [["temperature", "battery", "rssi"], [], []],
'MS2' : [["temperature", "humidity", "battery", "rssi"], [], []],
'S-MATE' : [["rssi"], ["three btn switch left", "three btn switch middle", "three btn switch right"], []],
'R5' : [["rssi"], ["six btn switch top left", "six btn switch top middle", "six btn switch top right", "six btn switch bottom left", "six btn switch bottom middle", "six btn switch bottom right"], []],
}

# Sensor manufacturer dictionary
Expand Down Expand Up @@ -2286,6 +2354,8 @@ class BLEMonitorBinarySensorEntityDescription(
'ST10' : 'MOCREO',
'MS1' : 'MOCREO',
'MS2' : 'MOCREO',
'S-MATE' : 'Sonoff',
'R5' : 'Sonoff',
}


Expand Down Expand Up @@ -2527,6 +2597,7 @@ class BLEMonitorBinarySensorEntityDescription(
"Sensirion",
"SensorPush",
"SmartDry",
"Sonoff",
"Switchbot",
"Teltonika",
"Thermobeacon",
Expand Down
6 changes: 6 additions & 0 deletions custom_components/ble_monitor/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -405,6 +405,12 @@ class BaseSensor(RestoreSensor, SensorEntity):
# | | |**four btn switch 2
# | | |**four btn switch 3
# | | |**four btn switch 4
# | | |**six btn switch top left
# | | |**six btn switch top middle
# | | |**six btn switch top right
# | | |**six btn switch bottom left
# | | |**six btn switch bottom middle
# | | |**six btn switch bottom right
# | |--BaseRemoteSensor (Class)
# | | |**remote
# | | |**fan remote
Expand Down
41 changes: 41 additions & 0 deletions custom_components/ble_monitor/test/test_sonoff.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
"""The tests for the Sonoff ble_parser."""
from ble_monitor.ble_parser import BleParser


class TestSonoff:
"""Tests for the HHCC parser"""
def test_sonoff_s_mate(self):
"""Test Sonoff BLE parser for S-MATE"""
data_string = "043e2b020103011122334455661f0201021b05ffffee1bc878f64a4690dd5ad9e71f4e4177f011694babb7fe68ba"
data = bytes(bytearray.fromhex(data_string))

# pylint: disable=unused-variable
ble_parser = BleParser()
sensor_msg, tracker_msg = ble_parser.parse_raw_data(data)

assert sensor_msg["firmware"] == "Sonoff"
assert sensor_msg["type"] == "S-MATE"
assert sensor_msg["mac"] == "00005AD9E71F"
assert sensor_msg["packet"] == 91
assert sensor_msg["data"]
assert sensor_msg["three btn switch left"] == "toggle"
assert sensor_msg["button switch"] == "single press"
assert sensor_msg["rssi"] == -70

def test_sonoff_r5(self):
"""Test Sonoff BLE parser for R5"""
data_string = "043e2b020103011122334455661f0201021B05FFFFEE1BC878F64A4790365AD509227B7442C5245C7DE4828B98ae"
data = bytes(bytearray.fromhex(data_string))

# pylint: disable=unused-variable
ble_parser = BleParser()
sensor_msg, tracker_msg = ble_parser.parse_raw_data(data)

assert sensor_msg["firmware"] == "Sonoff"
assert sensor_msg["type"] == "R5"
assert sensor_msg["mac"] == "00005AD50922"
assert sensor_msg["packet"] == 801
assert sensor_msg["data"]
assert sensor_msg["six btn switch top left"] == "toggle"
assert sensor_msg["button switch"] == "single press"
assert sensor_msg["rssi"] == -82
36 changes: 36 additions & 0 deletions docs/_devices/Sonoff_R5.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
---
manufacturer: Sonoff
name: SwitchMan R5 Scene Controller
model: R5 / R5W
image: Sonoff_R5.png
physical_description:
broadcasted_properties:
- six btn switch top left
- six btn switch top middle
- six btn switch top right
- six btn switch bottom left
- six btn switch bottom middle
- six btn switch bottom right
- button switch
- rssi
broadcasted_property_notes:
- property: six btn switch top left
note: returns 'short press', 'double press' or 'long press'
- property: six btn switch top middle
note: returns 'short press', 'double press' or 'long press'
- property: six btn switch top right
note: returns 'short press', 'double press' or 'long press'
- property: six btn switch bottom left
note: returns 'short press', 'double press' or 'long press'
- property: six btn switch bottom middle
note: returns 'short press', 'double press' or 'long press'
- property: six btn switch bottom right
note: returns 'short press', 'double press' or 'long press'
broadcast_rate:
active_scan:
encryption_key: Yes
custom_firmware:
notes:
- There are two versions of this switch - black and white.
- The switch sensor state will return to `no press` after the time set with the [reset_timer](configuration_params#reset_timer) option. It is advised to change the reset time to 1 second (default = 35 seconds).
---
27 changes: 27 additions & 0 deletions docs/_devices/Sonoff_S-MATE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
---
manufacturer: Sonoff
name: S-MATE Extreme Switch Mate | S-MATE2
model: S-MATE / S-MATE2
image: Sonoff_S-MATE.png
physical_description:
broadcasted_properties:
- three btn switch left
- three btn switch middle
- three btn switch right
- button switch
- rssi
broadcasted_property_notes:
- property: three btn switch left
note: returns 'short press', 'double press' or 'long press'
- property: three btn switch middle
note: returns 'short press', 'double press' or 'long press'
- property: three btn switch right
note: returns 'short press', 'double press' or 'long press'
broadcast_rate:
active_scan:
encryption_key: Yes
custom_firmware:
notes:
- There are two revisions of this switch - with and without power pass-through.
- The switch sensor state will return to `no press` after the time set with the [reset_timer](configuration_params#reset_timer) option. It is advised to change the reset time to 1 second (default = 35 seconds).
---
Binary file added docs/assets/images/Sonoff_R5.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added docs/assets/images/Sonoff_S-MATE.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading