Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add passive support for GV5121/GV5122/GV5123/GV5125/GV5126 #89

Merged
merged 1 commit into from
Jul 16, 2024
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
67 changes: 43 additions & 24 deletions src/govee_ble/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -171,42 +171,61 @@ def _process_mfr_data(
data = data[:-25]
msg_length = len(data)
if debug_logging:
_LOGGER.debug("Cleaned up packet: %s %s", mgr_id, hex(data))

if msg_length == 24 and (
(is_5121 := "GV5121" in local_name)
or (is_5122 := "GV5122" in local_name)
or (is_5123 := "GV5123" in local_name)
or (is_5125 := "GV5125" in local_name)
or (is_5126 := "GV5126" in local_name)
):
sensor_type = SensorType.BUTTON
if is_5121:
self.set_device_type("H5121")
sensor_type = SensorType.MOTION
elif is_5122:
self.set_device_type("H5122")
elif is_5123:
self.set_device_type("H5123")
sensor_type = SensorType.WINDOW
elif is_5125:
self.set_device_type("H5125")
elif is_5126:
self.set_device_type("H5126")
b_front_of_device_id = data[:2]
assert b_front_of_device_id
_LOGGER.debug("Cleaned up packet: %s %s", mgr_id, data.hex())

if msg_length == 24:
front_of_device_id = data[:2]
assert front_of_device_id
time_ms = data[2:6]
enc_data = data[6:22]
enc_crc = data[22:24]
if not calculate_crc(enc_data) == int.from_bytes(enc_crc, "big"):
_LOGGER.warning("CRC check failed for H512x: %s", hex(data))
return

key = time_ms + bytes(12)
try:
decrypted = decrypt_data(key, enc_data)
except ValueError:
_LOGGER.warning("Failed to decrypt H512x: %s", hex(data))
return
model_id = decrypted[2]
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@gh2o model_id is a guess here. It seems to hold up as I bought multiple of the same device to confirm it, but it would be great if you had some insight here since I feel like I'm poking around in the dark with this one.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I only have the H5122 (single button), and the FW appears to hard-code decrypted[0/1/2/3] so I'm not entirely sure what the meaning of these bytes are.

One possible hint though is that the FW code does different operations to assign to those bytes. To summarize, it does something like this:

decrypted[0] = ord('1') - 0x30
decrypted[1] = ord('5') - 0x30
decrypted[2] = 8
decrypted[3] = 2

If I had to venture a wild guess, the meaning of the numbers would be:

  • decrypted[0] = version (major)
  • decrypted[1] = version (minor)
  • decrypted[2] = model ID
  • decrypted[3] = number of data bytes following this byte

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it make sense to do

"GV5121" in local_name and model_id == 3

instead of or so that if this invariant breaks in the future, we can catch it?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The local name is empty when the scanner is passive

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I bought another motion sensor and 2 button remote. This pattern held true so I think we are ok here at least for now

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great, good to know.

# GV5121
# 01040302640100000000000000000000

# GV5122
# 01050802640000000000000000000000
# 01050802640000000000000000000000

# GV5123
# 01050202640200000000000000000000

# GV5125
# 01010a02640000000000000000000000

# GV5126
# 01010b02640100000000000000000000
sensor_type = SensorType.BUTTON
if "GV5121" in local_name or model_id == 3:
self.set_device_type("H5121")
self.set_device_name(f"5121{short_address(address)}")
sensor_type = SensorType.MOTION
elif "GV5122" in local_name or model_id == 8:
self.set_device_type("H5122")
self.set_device_name(f"5122{short_address(address)}")
elif "GV5123" in local_name or model_id == 2:
self.set_device_type("H5123")
self.set_device_name(f"5123{short_address(address)}")
sensor_type = SensorType.WINDOW
elif "GV5125" in local_name or model_id == 10:
self.set_device_type("H5125")
self.set_device_name(f"5125{short_address(address)}")
elif "GV5126" in local_name or model_id == 11:
self.set_device_type("H5126")
self.set_device_name(f"5126{short_address(address)}")
else:
return

battery_percentage = decrypted[4]
button_number_pressed = decrypted[5]
self.update_predefined_sensor(
Expand Down
140 changes: 133 additions & 7 deletions tests/test_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -662,6 +662,28 @@
service_uuids=[],
source="24:4C:AB:03:E6:B8",
)
GV5122_PASSIVE_SERVICE_INFO = BluetoothServiceInfo(
name="D2:32:39:37:56:34",
address="D2:32:39:37:56:34",
rssi=-68,
manufacturer_data={
61320: b'\xf3\n\x00$\xaa\xea\xa5c\x1b\x81\x08\x99\xe1\xc4\xe1@\x98\x83\xfe"Y5\xc4d'
},
service_data={},
service_uuids=[],
source="08:3A:F2:7B:50:9C",
)
GV5122_PASSIVE_2_SERVICE_INFO = BluetoothServiceInfo(
name="D2:32:39:37:56:34",
address="D2:32:39:37:56:34",
rssi=-68,
manufacturer_data={
61320: b"\xfe~\x00\x00\tL\xa8j\x1a\xf0\xd2\xbcD&\x0b\xd5\xaf4L\x0b\xe5\xc7\xf1\n"
},
service_data={},
service_uuids=[],
source="08:3A:F2:7B:50:9C",
)


def test_can_create():
Expand Down Expand Up @@ -1648,7 +1670,7 @@ def test_gvh5125_button_0():
title=None,
devices={
None: SensorDeviceInfo(
name="51255367",
name="51250F45",
model="H5125",
manufacturer="Govee",
sw_version=None,
Expand Down Expand Up @@ -1700,7 +1722,7 @@ def test_gvh5125_button_1():
title=None,
devices={
None: SensorDeviceInfo(
name="51255367",
name="51250F45",
model="H5125",
manufacturer="Govee",
sw_version=None,
Expand Down Expand Up @@ -1752,7 +1774,7 @@ def test_gvh5122_button_0():
title=None,
devices={
None: SensorDeviceInfo(
name="51225634",
name="51220F45",
model="H5122",
manufacturer="Govee",
sw_version=None,
Expand Down Expand Up @@ -1796,6 +1818,110 @@ def test_gvh5122_button_0():
)


def test_gvh5122_passive_button_0():
parser = GoveeBluetoothDeviceData()
service_info = GV5122_PASSIVE_SERVICE_INFO
result = parser.update(service_info)
assert result == SensorUpdate(
title=None,
devices={
None: SensorDeviceInfo(
name="51225634",
model="H5122",
manufacturer="Govee",
sw_version=None,
hw_version=None,
)
},
entity_descriptions={
DeviceKey(key="battery", device_id=None): SensorDescription(
device_key=DeviceKey(key="battery", device_id=None),
device_class=SensorDeviceClass.BATTERY,
native_unit_of_measurement=Units.PERCENTAGE,
),
DeviceKey(key="signal_strength", device_id=None): SensorDescription(
device_key=DeviceKey(key="signal_strength", device_id=None),
device_class=SensorDeviceClass.SIGNAL_STRENGTH,
native_unit_of_measurement=Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
),
},
entity_values={
DeviceKey(key="battery", device_id=None): SensorValue(
device_key=DeviceKey(key="battery", device_id=None),
name="Battery",
native_value=100,
),
DeviceKey(key="signal_strength", device_id=None): SensorValue(
device_key=DeviceKey(key="signal_strength", device_id=None),
name="Signal " "Strength",
native_value=-68,
),
},
binary_entity_descriptions={},
binary_entity_values={},
events={
DeviceKey(key="button_0", device_id=None): Event(
device_key=DeviceKey(key="button_0", device_id=None),
name="Button " "0",
event_type="press",
event_properties=None,
)
},
)


def test_gvh5122_passive_2_button_0():
parser = GoveeBluetoothDeviceData()
service_info = GV5122_PASSIVE_2_SERVICE_INFO
result = parser.update(service_info)
assert result == SensorUpdate(
title=None,
devices={
None: SensorDeviceInfo(
name="51225634",
model="H5122",
manufacturer="Govee",
sw_version=None,
hw_version=None,
)
},
entity_descriptions={
DeviceKey(key="battery", device_id=None): SensorDescription(
device_key=DeviceKey(key="battery", device_id=None),
device_class=SensorDeviceClass.BATTERY,
native_unit_of_measurement=Units.PERCENTAGE,
),
DeviceKey(key="signal_strength", device_id=None): SensorDescription(
device_key=DeviceKey(key="signal_strength", device_id=None),
device_class=SensorDeviceClass.SIGNAL_STRENGTH,
native_unit_of_measurement=Units.SIGNAL_STRENGTH_DECIBELS_MILLIWATT,
),
},
entity_values={
DeviceKey(key="battery", device_id=None): SensorValue(
device_key=DeviceKey(key="battery", device_id=None),
name="Battery",
native_value=100,
),
DeviceKey(key="signal_strength", device_id=None): SensorValue(
device_key=DeviceKey(key="signal_strength", device_id=None),
name="Signal " "Strength",
native_value=-68,
),
},
binary_entity_descriptions={},
binary_entity_values={},
events={
DeviceKey(key="button_0", device_id=None): Event(
device_key=DeviceKey(key="button_0", device_id=None),
name="Button " "0",
event_type="press",
event_properties=None,
)
},
)


def test_gvh5123_open():
parser = GoveeBluetoothDeviceData()
service_info = GV5123_OPEN_SERVICE_INFO
Expand All @@ -1804,7 +1930,7 @@ def test_gvh5123_open():
title=None,
devices={
None: SensorDeviceInfo(
name="51230B3D",
name="51230F45",
model="H5123",
manufacturer="Govee",
sw_version=None,
Expand Down Expand Up @@ -1860,7 +1986,7 @@ def test_gvh5123_closed():
title=None,
devices={
None: SensorDeviceInfo(
name="51230B3D",
name="51230F45",
model="H5123",
manufacturer="Govee",
sw_version=None,
Expand Down Expand Up @@ -1917,7 +2043,7 @@ def test_gvh5121_motion():
title=None,
devices={
None: SensorDeviceInfo(
name="5121195A",
name="51210F45",
model="H5121",
manufacturer="Govee",
sw_version=None,
Expand Down Expand Up @@ -1970,7 +2096,7 @@ def test_gvh5121_motion_2():
title=None,
devices={
None: SensorDeviceInfo(
name="5121195A",
name="51210F45",
model="H5121",
manufacturer="Govee",
sw_version=None,
Expand Down
Loading