Skip to content

Commit

Permalink
Support GVH5184 (#12)
Browse files Browse the repository at this point in the history
Govee 4 probe meat thermometer
  • Loading branch information
ElksInNC committed Jan 11, 2022
1 parent 1435062 commit ccdb519
Show file tree
Hide file tree
Showing 6 changed files with 256 additions and 3 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ If you use HA discovery, devices should appear under MQTT Devices NOT the Tasmot
| `'ATCmi'` | `'A4C138XXXXXX'` | Xiaomi sensors on ATC or pvvx firmware with "Mi" advertisement |
| `'GVH5075'` | `'A4C138XXXXXX'` | Govee H5075. Should work for H5072 as well (untested). |
| `'GVH5183'` | `'A4C138XXXXXX'` | Govee H5183 single probe meat thermometer. |
| `'GVH5184'` | `'D03232XXXXXX/1'` | Govee H5184 four probe meat thermometer with display. |
| `'IBSTH2'` | `'494208XXXXXX'` | Inkbird IBSTH2 with and without humidity. Should work for IBSTH1 as well (untested). |
| `'WoSensorTH'` | `'D4E4A3XXXXXX/1'` | Switchbot temperature and humidity sensor. |
| `'WoContact'` | `'D4BD28XXXXXX/1'` | Switchbot contact sensor (also has motion, binary lux, and a button). |
Expand Down
1 change: 1 addition & 0 deletions blerry/blerry.be
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
var user_config = {'A4C138AAAAAA': {'alias': 'trial_govee5075', 'model': 'GVH5075', 'discovery': true},
'A4C138BBBBBB': {'alias': 'other_govee5075', 'model': 'GVH5075', 'via_pubs': false},
'A4C138XXXXXX': {'alias': 'govee5183meats', 'model': 'GVH5183'},
'D03232XXXXXX/1': {'alias': 'govee5184-4probe-meats', 'model': 'GVH5184'},
'A4C138CCCCCC': {'alias': 'trial_ATCpvvx', 'model': 'ATCpvvx', 'discovery': true, 'use_lwt': true},
'A4C138CCCCCC': {'alias': 'ATC_on_milike', 'model': 'ATCmi'},
'494208DDDDDD': {'alias': 'trial_inkbird', 'model': 'IBSTH2', 'discovery': true},
Expand Down
3 changes: 2 additions & 1 deletion blerry/blerry_main.be
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,7 @@ end
# Load model handle functions only if used
var model_drivers = {'GVH5075' : 'blerry_model_GVH5075.be',
'GVH5183' : 'blerry_model_GVH5183.be',
'GVH5184' : 'blerry_model_GVH5184.be',
'ATCpvvx' : 'blerry_model_ATCpvvx.be',
'ATC' : 'blerry_model_ATCpvvx.be',
'pvvx' : 'blerry_model_ATCpvvx.be',
Expand Down Expand Up @@ -145,4 +146,4 @@ tasmota.cmd('BLEDetails4')
def DetailsBLE_callback(value, trigger, msg)
device_config[value['mac']]['handle'](value, trigger, msg)
end
tasmota.add_rule(details_trigger, DetailsBLE_callback)
tasmota.add_rule(details_trigger, DetailsBLE_callback)
100 changes: 100 additions & 0 deletions blerry/blerry_model_GVH5184.be
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
# The 5184 uses a two line method. Probe1/2 are published in one packet. Probe3/4 in another. Not always sequential
def handle_GVH5184(value, trigger, msg)
if trigger == details_trigger
var this_device = device_config[value['mac']]
var p = bytes(value['p'])
var i = 0
var adv_len = 0
var adv_data = bytes('')
var adv_type = 0
while i < size(p)
adv_len = p.get(i,1)
adv_type = p.get(i+1,1)
adv_data = p[i+2..i+adv_len]
if (adv_type == 0xFF) && (adv_len == 0x14)
var this_part_data = [adv_data.get(9, -1), adv_data.get(10, -2), adv_data.get(12, -2), adv_data.get(14, -1), adv_data.get(15, -2), adv_data.get(17, -2)]
var last_full_data = this_device['last_p']
var this_full_data = [ 0, [-1, 0, 0, 0, 0, 0], [-1, 0, 0, 0, 0, 0], 0 ]
var seq_num = adv_data.get(8, -1)
if last_full_data == bytes('')
last_full_data = [ -1, [-1, 0, 0, 0, 0, 0], [-1, 0, 0, 0, 0, 0], 0 ]
else
this_full_data = last_full_data
end
if (this_part_data == last_full_data[seq_num]) && (last_full_data[3] < 31)
this_full_data[3]=this_full_data[3]+1
device_config[value['mac']]['last_p'] = this_full_data.copy()
return 0
end
this_full_data[0]= adv_data.get(7, -1)
this_full_data[3] = 0
this_full_data[seq_num]=this_part_data.copy()
device_config[value['mac']]['last_p'] = this_full_data.copy()
if (this_full_data[1][0] < 0) || (this_full_data[2][0]) < 0
return 0
end
if this_device['discovery'] && !this_device['done_disc']
for j: 1 .. 4
publish_binary_sensor_discovery(value['mac'], ('Temperature_'+str(j)+'_Status'), 'plug')
publish_binary_sensor_discovery(value['mac'], 'Temperature_'+str(j)+'_Alarm', 'heat')
publish_sensor_discovery(value['mac'], 'Temperature_'+str(j), 'temperature', '°C')
publish_sensor_discovery(value['mac'], 'Temperature_'+str(j)+'_Target', 'temperature', '°C')
end
publish_sensor_discovery(value['mac'], 'Battery', 'battery', '%')
publish_sensor_discovery(value['mac'], 'RSSI', 'signal_strength', 'dB')
device_config[value['mac']]['done_disc'] = true
end
var output_map = {}
output_map['Time'] = tasmota.time_str(tasmota.rtc()['local'])
output_map['alias'] = this_device['alias']
output_map['mac'] = value['mac']
output_map['via_device'] = device_topic
output_map['RSSI'] = value['RSSI']
if this_device['via_pubs']
output_map['Time_via_' + device_topic] = output_map['Time']
output_map['RSSI_via_' + device_topic] = output_map['RSSI']
end
output_map['Battery'] = math.ceil(this_full_data[0]/255.0*100.0)
for j:1 .. 2
for k:j-1 .. j
var probeset
if (this_full_data[j][3+((k-j)*3)] & 0x80) >> 7
probeset = ['ON', 'OFF']
else
probeset = ['OFF', 'OFF']
end
if (this_full_data[j][3+((k-j)*3)] & 0x40) >> 6
probeset = ['ON', 'ON']
end
if this_full_data[j][4+((k-j)*3)]==65535
probeset = probeset .. 'unavailable'
else
probeset = probeset .. round(this_full_data[j][4+((k-j)*3)]/100.0, this_device['temp_precision'])
end
if this_full_data[j][5+((k-j)*3)]==65535
probeset = probeset .. 'unavailable'
else
probeset = probeset .. round(this_full_data[j][5+((k-j)*3)]/100.0, this_device['temp_precision'])
end
output_map['Temperature_'+str(j+k)+'_Status'] = probeset[0]
output_map['Temperature_'+str(j+k)+'_Alarm'] = probeset[1]
output_map['Temperature_'+str(j+k)] = probeset[2]
output_map['Temperature_'+str(j+k)+'_Target'] = probeset[3]
end
end
var this_topic = base_topic + '/' + this_device['alias']
tasmota.publish(this_topic, json.dump(output_map), this_device['sensor_retain'])
if this_device['publish_attributes']
for output_key:output_map.keys()
tasmota.publish(this_topic + '/' + output_key, string.format('%s', output_map[output_key]), this_device['sensor_retain'])
end
end
end
i = i + adv_len + 1
end
end
end

# map function into handles array
device_handles['GVH5184'] = handle_GVH5184
require_active['GVH5184'] = false
9 changes: 7 additions & 2 deletions dev_helpers/spoof_devices.be
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,13 @@ fake_ble.every_second = def ()
# tasmota.publish_result('{\"DetailsBLE\":{\"mac\":\"494207BBBBBB\",\"a\":\"inkbirdth_active\",\"RSSI\":-83,\"p\":\"0201060302F0FF04097370730AFF3E09F11000526A6408\"}}','test')
# tasmota.publish_result('{\"DetailsBLE\":{\"mac\":\"494207CCCCCC\",\"a\":\"inkbirdth_active_noH\",\"RSSI\":-83,\"p\":\"0201060302F0FF04097470730AFF5CFA000000C4D54908\"}}','test')
# tasmota.publish_result('{\"DetailsBLE\":{\"mac\":\"D4E4A3AAAAAA\",\"a\":\"sbot_temp_passive\",\"RSSI\":-75,\"p\":\"02010609FF5900D4E4A3AAAAAA\"}}','test')
tasmota.publish_result('{\"DetailsBLE\":{\"mac\":\"D4E4A3BBBBBB/1\",\"a\":\"sbot_temp_active\",\"RSSI\":-83,\"p\":\"02010609FF5900D4E4A3BBBBBB11071BC5D5A50200B89FE6114D22000DA2CB0916000D5410640411BC\",\"0x0d00\":\"5410640511BC\"}}','test')
# tasmota.publish_result('{\"DetailsBLE\":{\"mac\":\"D4E4A3BBBBBB/1\",\"a\":\"sbot_temp_active\",\"RSSI\":-83,\"p\":\"02010609FF5900D4E4A3BBBBBB11071BC5D5A50200B89FE6114D22000DA2CB0916000D5410640411BC\",\"0x0d00\":\"5410640511BC\"}}','test')
if (tasmota.millis()%2) == 1
tasmota.publish_result('{\"DetailsBLE\":{\"mac\":\"D03232XXXXXX/1\",\"a\":\"govee5184_seq1\",\"RSSI\":-83,\"p\":\"0201060303518414FF363E5D01000101BC01860898FFFF06FFFFFFFF"}}','test')
else
tasmota.publish_result('{\"DetailsBLE\":{\"mac\":\"D03232XXXXXX/1\",\"a\":\"govee5184_seq2\",\"RSSI\":-81,\"p\":\"0201060303518414FF363E5D01000101BB02860960FFFFC609600898"}}','test')
end
fake_print_time = tasmota.millis(5000)
end
end
tasmota.add_driver(fake_ble)
tasmota.add_driver(fake_ble)
145 changes: 145 additions & 0 deletions docs/GVH5184.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
Working notes - reverse engineering data stream for GVH5184


Working on the code for the 4-probe Govee driver. NOTE - any code is intended as psuedo-code and is not syntax correct.

The full 'p' string that comes from BLE looks like this:

0201060303518414FF363E5D01000101E40106FFFFFFFF06FFFFFFFF

Breaking that down: (ref - https://community.silabs.com/s/article/kba-bt-0201-bluetooth-advertising-data-basics?language=en_US)

```
02 01 06 03 03 51 84 14 FF
02 01 06
-- -- -- <- Two bytes (02), Type = Flags (01), Value (06) (bitwise - 00000110) Bit 1 : “LE General Discoverable Mode”
Bit 2: “BR/EDR Not Supported.”
03 03 51 84
-- -- -- -- <- Three bytes (03), Type = Service Class (03), Little endian. 84 51 - not in the list of 16Bit UUIDs. Perhaps Manuf cheated and just put model number here instead. It is the 5184 sensor.
14 FF
-- -- <- 20 Bytes (0x14), Type = FF - Manuf. Proprietary data
This is what we were looking for. All data after the 14 FF will be data we can break up for sensor data.
```

The remaining string to be manipulated:

36 3E 5D 01 00 01 01 E4 01 06 FF FF FF FF 06 FF FF FF FF

```
1 2 3 (Byte Sequence)
36 3E 5D
-- -- -- <- Last 3 bytes of MAC ADDRESS
4 5 6 7
01 00 01 01
-- -- -- -- <- static, unknown, not needed at this time
8
E4
-- <- Battery Percentage Value divided by 255 for percentage.
9
01
-- <- Sequence number. Can be 01 or 02. 01 is Probe1&2. 02 is Probe3&4.
10
06
-- <- Probe1/3 status. 06 is no probe. 86 is probe inserted. C6 when probe has exceeded setpoint
11 12
FF FF
-- -- <- Probe1/3 temp. FF FF if not inserted. Big-endian 2-byte number. See below.
13 14
FF FF
-- -- <- Probe1/3 set point for alarm. Same scheme as temp.
15
06
-- <- Probe2/4 status. 06 is no probe. 86 is probe inserted.
16 17
FF FF
-- -- <- Probe2/4 temp. Same scheme as Probe 1/3
18 19
FF FF
-- -- <- Probe2/4 set point for alarm. Same scheme as temp.
```

We now can build out a sensor data stream to encode and push back through MQTT

```
BYTE8 - Battery %. 0x00-0xFF, 0-255 - Battery percentage is (VALUE / 255)
BYTE9 - Sequence. Either 01 or 02. 01 represents Probes 1 & 2. 02 represents probes 3 & 4.
BYTE10 - Probe[A] Status. [A] is 1 when Sequence is 1. [A] is 3 when Sequence is 2.
Possible Values. 06 - no probe inserted. 86 - Probe inserted normal function. C6 - probe temp has exceeded set point.
BYTE11-12 - Probe[A] Temp. Two bytes - big endian - convert to decimal and divide by 100 to obtain temperature in (C).
BYTE13-14 - Probe[A] Setpoint. Two bytes - big endian. Same conversion. Temp to alarm at. Will set status to C6 when temp >= setpoint
BYTE15 - Probe[B] Status. [A] is 1 when Sequence is 1. [A] is 3 when Sequence is 2.
Possible Values. 06 - no probe inserted. 86 - Probe inserted normal function. C6 - probe temp has exceeded set point.
BYTE16-17 - Probe[B] Temp. Two bytes - big endian - convert to decimal and divide by 100 to obtain temperature in (C).
BYTE18-19 - Probe[B] Setpoint. Two bytes - big endian. Same conversion. Temp to alarm at. Will set status to C6 when temp >= setpoint
```



Building on other driver files, the following approach will be used.

1) Iterate through the string and find 14 and FF. Assign the remaing 19 bytes to a variable.

2) We need to check if anything is changed so we don't do needless updates. So we will always need a last_value and current_value. If
they are the same we can end and not update anything.

3) Declare last_value = stored ['last_value']

4) Check to see if last_value is empty - which means we are on our first pass and need to create a placeholder

5) If last_value does not yet exist - declare "last_value" and assign negative values for later test.)
Last value will be a date structure list with the following values stored in the list. This is a list and not a map
so these names are just for documentation. It will be a list of 12 INT values.
```
[ Probe1 Status,
Probe1 Temp,
Probe1 Setpoint,
Probe2 Status,
Probe2 Temp,
Probe2 Setpoint
Probe3 Status,
Probe3 Temp,
Probe3 Setpoint,
Probe4 Status,
Probe4 Temp,
Probe4 Setpoint ]
```

5) Now we need to see if the current 'p' value is carrying the values for Probe1/2 or Probe3/4. Since we only get half the probes on any pass - we
have to parse out the values and push them into our current_value list based on which sequence we are on. Check for sequence.

```
If .get(8,-1) == 1 then #This is BYTE9 which contains the sequence number)
sequence_index=0
else
sequence_index=6
```

6) Now we can use sequence index as an offset. This will allow us to reuse the same block of code regardless of which sequence we are on.
```
current_value[sequence_index] = .get((9+sequence_index),-1)
current_value[sequence_index+1] = .get((10+sequence_index),-2)
current_value[sequence_index+2] = .get((12+sequence_index),-2)
current_value[sequence_index+3] = .get((14+sequence_index),-1)
current_value[sequence_index+4] = .get((15+sequence_index),-2)
current_value[sequence_index+5] = .get((17+sequence_index),-2)
```

7) Now we can compare current_value to last_value. Both contain a complete set of 12 discrete values. If the same return 0.

8) If they are not the same we will need to publish the updated data. At this point - it is unclear if there is any advantage or disadvantage of publishing
all 12 pieces of data. If we do discovery on Probe1/2 and Probe3/4 separtely and publish values for each separately - then we would not have entities for
Probe 3/4 if they are not plugged in. TBD.

0 comments on commit ccdb519

Please sign in to comment.