forked from plambrechtsen/pethublocal
-
Notifications
You must be signed in to change notification settings - Fork 0
/
pethubpacket.py
1296 lines (1221 loc) · 69.2 KB
/
pethubpacket.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#!/usr/bin/env python3
"""
Decode Sure Pet Packet
Copyright (c) 2021, Peter Lambrechtsen ([email protected])
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program; if not, write to the Free Software Foundation,
Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
"""
import binascii, struct, time, sys, sqlite3, json, glob, logging, pathlib, pytz, re
import os
from datetime import datetime
from operator import xor
from pathlib import Path
from enum import IntEnum
from datetime import datetime, date
from pethubconst import *
from box import Box
from configparser import ConfigParser
#Debugging mesages
PrintFrame = False #Print the before and after xor frame
LogFrame = False #Log the frames to a file
LogAirFrame = False #Log the frame sent over the air as a hub mqtt packet to a file
PrintFrameDbg = False #Print the frame headers
Print126Frame = False #Debug the 2A / 126 feeder frame
Print127Frame = False #Debug the 2D / 127 feeder frame
Print132Frame = True #Debug the 3C / 132 hub and door frame
PrintHubFrame = False #Debug the Hub frame
PrintFeederFrame = False #Debug the Hub frame
Print2Frame = False #Debug the 2 frame
PrintDebug = False #Debug the 2 frame
DebugResponse = False #Returning MSG payload in json response
'''
#Setup Logging framework to log to console without timestamps and log to file with timestamps
log = logging.getLogger('')
log.setLevel(logging.INFO)
logformat = logging.Formatter("%(asctime)s - [%(levelname)-5.5s] - %(message)s")
ch = logging.StreamHandler(sys.stdout)
log.addHandler(ch)
pathlib.Path("log").mkdir(exist_ok=True)
fh = logging.FileHandler('log/pethubpacket-{:%Y-%m-%d}.log'.format(datetime.now()))
fh.setFormatter(logformat)
log.addHandler(fh)
'''
#Load PetHubLocal database
def box_factory(cursor, row): #Return results as a Box/dict key value pair
d = {}
for idx, col in enumerate(cursor.description):
d[col[0]] = row[idx]
return Box(d)
#Load the database
if os.path.isfile('pethubtest.db'): #If we have a pethubtest then use it as it is for regression testing
pethubdb = "pethubtest.db"
elif os.path.isfile('pethublocal.db'):
pethubdb = 'pethublocal.db'
else:
#Database doesn't exist, so creating dummy one.
pethubdb = "pethublocal.db"
#print('Creating Pet Hub Local DB ' + pethubdb)
connection = sqlite3.connect(pethubdb)
cursor = connection.cursor()
pethublocal_file = open("pethublocal.sql")
cursor.executescript(pethublocal_file.read())
connection.close()
conn=sqlite3.connect(pethubdb)
conn.row_factory = box_factory
curs=conn.cursor()
def sqlcmd(sql_cmd):
try:
curs.execute(sql_cmd)
conn.commit()
except Error as e:
print(e)
def bit2int(number,start,bitlen,fill):
return str(int(number[start : start+bitlen],2)).zfill(fill)
def int2bit(number,fill):
return str(bin(int(number))[2:]).zfill(fill)
def hextimestampfromnow(): #Create UTC hex timestamp, used for the hub timestamp values for every event
return hex(round(datetime.utcnow().timestamp()))[2:]
def devicetimestamptostring(hexts):
# print("Incoming ts",tohex(hexts))
intts = int.from_bytes(hexts,byteorder='little')
binstring = str(bin(intts)[2:]).zfill(32)
ts="{}-{}-{} {}:{}:{}".format("20"+bit2int(binstring,0,6,2),bit2int(binstring,6,4,2),bit2int(binstring,10,5,2),bit2int(binstring,15,5,2),bit2int(binstring,20,6,2),bit2int(binstring,26,6,2))
return ts
def devicetimestampfromnow():
now = datetime.utcnow() # Current timestamp in UTC
bintime = int2bit(now.strftime("%y"),6)+int2bit(now.month,4)+int2bit(now.day,5)+int2bit(now.hour,5)+int2bit(now.minute,6)+int2bit(now.second,6)
return int(bintime,2).to_bytes(4,'little').hex() #Return as a hex string
def devicetimestampfromstring(tsstring):
tsarray=re.split('-| |:',tsstring)
#Take only the last two digits of the year if it is longer
bintime = int2bit(tsarray[0][-2:],6)+int2bit(tsarray[1],4)+int2bit(tsarray[2],5)+int2bit(tsarray[3],5)+int2bit(tsarray[4],6)+int2bit(tsarray[5],6)
return int(bintime,2).to_bytes(4,'little').hex() #Return as a hex string
def localtimestampfromnow():
dtnow = datetime.now()
return dtnow.strftime("%Y-%m-%d %H:%M:%S")
def chiptohex(chip):
chiphex = ""
if "." in chip:
#FDX-B Chip - Append 01 for chip type.
chipsplit=chip.split(".")
chipbin=format(int(chipsplit[0]),'b').zfill(10)+format(int(chipsplit[1]),'b').zfill(38)
chipint=int(chipbin,2)
chiphex=hex(int.from_bytes(chipint.to_bytes(6,'little'),'big'))[2:]
chiphex = chiphex+'01'
#print("Feeder Chip to Hex : " + chip + " " + chiphex)
elif len(chip) == 10:
#HDX Chip - Chip type seems to be always 03 and needs a 00 to pad it to the right length.
chiphex = chip+'0003'
else:
chiphex = "Error"
return chiphex
def bytestotag(tagbytes):
print(tohex(tagbytes))
if len(tagbytes) == 7:
if tagbytes[6] == 0x01: # FDX-B tag type 0x01
tagint = int.from_bytes(tagbytes[:6], byteorder='little')
if tagint > 0:
tagbin = "{0:48b}".format(tagint)
return str(int(tagbin[:10], 2)) + "." + str(int(tagbin[10:], 2)).zfill(12)
else:
return 'Empty'
elif tagbytes[6] == 0x03: # HDX Tag type 0x07
#HDX
return tohex(tagbytes[:5])
elif tagbytes[6] == 0x07: # Empty Tag 0x07
return "Empty"
elif tagbytes[6] == 0x00: # Empty Tag 0x00
return "Empty"
else:
return tohex(tagbytes)
def doorchiptohex(chip):
chipsplit=chip.split(".")
chipbin=format(int(chipsplit[0]),'b').zfill(10)+format(int(chipsplit[1]),'b').zfill(38)
#print(chipbin)
chiphex='01' + hex(int(chipbin[::-1],2))[2:]
#print("Door Chip to Hex : " + chip + " " + chiphex)
return chiphex
def doorhextochip(chiphex):
if int(chiphex,16) == 0:
chip = "Null" #**TODO Need to figure out how to calculate this
else:
chipbin = "{0:48b}".format(int.from_bytes(bytes.fromhex(chiphex), byteorder='big'))[::-1]
chip=str(int(chipbin[:10],2)) + "." + str(int(chipbin[10:],2)).zfill(12)
return chip
def splitbyte(bytestring):
return " ".join(bytestring[i:i+2] for i in range(0, len(bytestring), 2))
def bltoi(value): #Bytes little to integer
return int.from_bytes(value,byteorder='little')
#Conversion of byte arrays into integers
def b2ih(b2ihvalue):
#Divide int by 100 to give two decimal places for weights
return str(int(b2is(b2ihvalue))/100)
def b2iu(b2ivalue):
#Take little endian hex byte array and convert it into a int then into a string.
return str(int.from_bytes(b2ivalue, byteorder='little', signed=False))
def b2is(b2ivalue):
#Take little endian hex byte array and convert it into a int then into a string.
return str(int.from_bytes(b2ivalue, byteorder='little', signed=True))
def b2ibs(b2ivalue):
#Take little endian hex byte array and convert it into a int then into a string.
return str(int.from_bytes(b2ivalue, byteorder='big', signed=True))
def b2ibu(b2ivalue):
#Take little endian hex byte array and convert it into a int then into a string.
return str(int.from_bytes(b2ivalue, byteorder='big', signed=False))
def tohex(ba):
return ''.join(format(x, '02x') for x in ba)
#Convert a int to hex
def hb(hexbyte):
return format(hexbyte,'02x')
def converttime(timearray):
#Seems that the minutes returned are in the upper byte, so need to subtract 128.
if timearray[1] >= 128:
timearray[1] -= 128
return ':'.join(format(x, '02d') for x in timearray)
def converttimetominutes(timearray):
#Seems that the minutes returned are in the upper byte, so need to subtract 128.
if timearray[1] >= 128:
timearray[1] -= 128
return str((timearray[0]*60)+timearray[1])
def converttimetohex(timestring): #For curfew time
time=timestring.split(':')
return hb(int(time[0]))+" "+hb(int(time[1]))
def petnamebydevice(mac_address, deviceindex):
curs.execute('select tag from tagmap where mac_address=(?) and deviceindex=(?)', (mac_address, deviceindex))
tagval = curs.fetchone()
if (tagval):
curs.execute('select name from pets where tag=(?)', ([tagval.tag]))
petval = curs.fetchone()
if (petval):
petname=petval.name
else:
petname="Unknown"
else:
petname="Unknown"
return petname
#Parse Feeder and Cat Flap Multi-Frame
#126 Frames aka can have multiple messages, so need to loop until you get to the end
def parsemultiframe(device, payload):
response = []
operation = []
while len(payload) > 2:
subframelength=payload[0]+1
currentframe=payload[1:subframelength]
frameresponse = parseframe(device, currentframe)
response.append(frameresponse)
#Append the operation to a separate array we attach at the end.
operation.append(frameresponse.Operation)
#Remove the parsed payload and loop again
del payload[0:subframelength]
response.append({"Operation":operation})
return response
#Parse Feeder, Cat Flap and Felaqua Frame
#Single frame payload to be parsed, can be called by 126 Multi data frame or 127 Single command frame
def parseframe(device, value):
frameresponse = Box()
#Frame timestamp value
frameresponse.frametimestamp = devicetimestamptostring(value[4:8])
# print("Timestampts:",frameresponse.framets)
if DebugResponse:
frameresponse.Message = tohex(value)
#Return the message type and counter which is two bytes as they are needed for acknowledgement of the message back
frameresponse.data=Box({'msg':hb(value[0]),'counter':b2iu(value[2:4])})
if value[0] in [0x0b, 0x10, 0x16]: #Unknown messages
op=hb(value[0])
frameresponse.Operation="Msg"+op
frameresponse.Message=tohex(value[3:])
elif value[0] == 0x00: #Send Acknowledge for message type
frameresponse.Operation="Ack"
frameresponse.Message=hb(value[8])
if Print126Frame:
print("FF-00:ACK-" + hb(value[8]))
elif value[0] == 0x01: #Send query for data type
frameresponse.Operation="Query"
frameresponse.Type=hb(value[8])
frameresponse.SubData=tohex(value[9:])
if Print126Frame:
print("FF-01:QRY-" + hb(value[8]))
elif value[0] == 0x07: #Set Time
frameresponse.Operation="Time"
frameresponse.Type=tohex(value[8:])
elif value[0] == 0x09: #Update or query config registers with subtypes depending on device type
frameresponse.Operation="UpdateState"
submessagevalue = b2is(value[9:12])
if value[8]==0x05: # Training mode
frameresponse.SubOperation="Training"
frameresponse.Mode=submessagevalue
elif value[8]==0x0a: #Set Left Weight
frameresponse.SubOperation="SetLeftScale"
frameresponse.Weight=str(round(int(submessagevalue)/100))
elif value[8]==0x0b: #Set Right Weight
frameresponse.SubOperation="SetRightScale"
frameresponse.Weight=str(round(int(submessagevalue)/100))
elif value[8]==0x0c: #Set Bowl Count either 1 or 2
frameresponse.SubOperation="SetBowlCount"
frameresponse.Bowls=FeederBowls(int(submessagevalue)).name
elif value[8]==0x0d: #Set Feeder Close Delay
frameresponse.SubOperation="SetCloseDelay"
frameresponse.Delay=FeederCloseDelay(int(submessagevalue)).name
elif value[8]==0x12: # **TODO - Always seems to be the same value, either 500, or 5000
frameresponse.SubOperation="Set12"
frameresponse.Value = submessagevalue
frameresponse.MSG=tohex(value[9:])
elif value[8] == 0x14: # Custom Modes for Feeder
frameresponse.SubOperation = "Custom-" + FeederCustomMode(int(submessagevalue)).name
frameresponse.MODE = submessagevalue
frameresponse.MSG = FeederCustomMode(int(submessagevalue)).name
sqlcmd('UPDATE devices SET custommode=' + submessagevalue + ' WHERE mac_address = "' + device + '"')
elif value[8]==0x17: #Set ZeroLeftWeight
frameresponse.SubOperation="ZeroLeft"
frameresponse.WEIGHT=submessagevalue
elif value[8]==0x18: #Set ZeroRightWeight
frameresponse.SubOperation="ZeroRight"
frameresponse.WEIGHT=submessagevalue
elif value[8]==0x19: #SetTODO 19
frameresponse.SubOperation="SetTODO"
frameresponse.MSG=tohex(value[9:])
else:
frameresponse.SubOperation="SetTODO"
frameresponse.MSG=tohex(value[9:12])
elif value[0] == 0x0c: #Battery state for four bytes
frameresponse.Operation="Battery"
battery = str(int(b2is(value[8:12]))/1000)
frameresponse.Battery=battery
upd = "UPDATE devices SET battery=" + battery + ' WHERE mac_address = "' + device + '"'
curs.execute(upd)
conn.commit()
frameresponse.Value2 = str(int(b2is(value[12:16]))) #**TODO Not sure what this value is.
frameresponse.Value3 = str(int(b2is(value[16:20]))) #**TODO Or this one
frameresponse.BatteryTime = devicetimestamptostring(value[20:24]) #**TODO Last time the feeders time was set?
elif value[0] == 0x0d: #Lock state of Cat Flap and zeroing scales
print("Message 0d " , len(value))
if len(value) == 20: #Zeroing Scales
frameresponse.Operation="ZeroScales"
frameresponse.Scale = FeederZeroScales(int(value[19])).name
else:
frameresponse.Operation="CurfewLockState"
frameresponse.LockState = CatFlapLockState(int(value[29])).name
frameresponse.LockStateNumber = str(PetDoorLockState[frameresponse.LockState].value)
#frameresponse.MSG = tohex(value)
elif value[0] == 0x11: #Provision tag to device and set lock states on cat flap.
if value[16] == 0x00 and TagState(value[17]).name == "LockState": #CatFlap Status Update on offset 0 and TagState = 2
frameresponse.Operation="LockState"
frameresponse.Animal = "Empty"
frameresponse.Offset = value[16]
frameresponse.LockState=CatFlapLockState(int(value[15])).name
frameresponse.LockStateNumber=str(PetDoorLockState[frameresponse.LockState].value)
sqlcmd('UPDATE doors SET lockingmode=' + frameresponse.LockStateNumber + ' WHERE mac_address = "' + device + '"')
elif value[14] in [0x01,0x03,0x07]: #Provisioning HDX (1) or FDX-B (3) chip
frameresponse.Operation="Tag"
tag = bytestotag(value[8:15])
if tag == "Empty":
frameresponse.Animal = tag
else:
curs.execute('select name from pets where tag=(?)', ([tag]))
petval = curs.fetchone()
if petval:
frameresponse.Animal=petval.name
else:
frameresponse.Animal=tag
#Setting Lock State on the Animal rather than the Door
frameresponse.LockState=CatFlapLockState(int(value[15])).name
frameresponse.LockStateNumber=str(int(value[15]))
frameresponse.Offset=value[16]
frameresponse.TagState=TagState(value[17]).name
elif value[14] == 0x07: #Set Cat Flap Lock State
frameresponse.Operation="Tag"
frameresponse.Animal = "Empty"
frameresponse.Offset = value[16]
frameresponse.LockState=CatFlapLockState(int(value[15])).name
frameresponse.LockStateNumber=str(PetDoorLockState[frameresponse.LockState].value)
#Update sqlite with lock state integer value
else:
frameresponse.Operation="Unknown"
frameresponse.MSG=tohex(value)
elif value[0] == 0x12: #Curfew
frameresponse.Operation="Curfew"
frameresponse.Curfew=[]
curfewentries = value[16:]
curfewcount = 0
while len(curfewentries) > 4:
if CatFlapCurfewState(curfewentries[8]).name == 'On':
curfewentry = Box({"State":curfewentries[8],"Start":devicetimestamptostring(curfewentries[0:4]),"End":devicetimestamptostring(curfewentries[4:8])})
#print("Curfew Val ",curfewentries[8])
#print("Curfew Entry ",curfewentry)
frameresponse.Curfew.append(curfewentry)
curfewcount += 1
del curfewentries[0:9]
elif value[0] == 0x13: #Pet Movement through Cat door
tag = bytestotag(value[18:25])
if tag == "Empty":
frameresponse.Animal = tag
else:
curs.execute('select name from pets where tag=(?)', ([tag]))
petval = curs.fetchone()
if petval:
frameresponse.Animal = petval.name
else:
frameresponse.Animal = tag
AnimalDirection=(value[16] << 8) + value[17]
#print(AnimalDirection)
if CatFlapDirection.has_value(AnimalDirection):
frameresponse.Direction=CatFlapDirection(AnimalDirection).name
else:
frameresponse.Direction="**UNKNOWN**"
frameresponse.Operation="PetMovement"
frameresponse.NumberValue=b2iu(value[12:16])
frameresponse.OtherTS=b2iu(value[25:29])
#frameresponse.MSG=tohex(value)
elif value[0] == 0x18:
frameresponse.Operation="Feed"
#Hard code feeder states
if FeederState.has_value(value[15]):
action=FeederState(int(value[15])).name
feederopenseconds=b2iu(value[16:17])
scaleleftfrom=b2ih(value[19:23])
scaleleftto=b2ih(value[23:27])
scalerightfrom=b2ih(value[27:31])
scalerightto=b2ih(value[31:35])
scaleleftdiff=str(round(float(scaleleftto)-float(scaleleftfrom),2))
scalerightdiff=str(round(float(scalerightto)-float(scalerightfrom),2))
frameresponse.Action=action
frameresponse.Time=feederopenseconds
frameresponse.LeftFrom=scaleleftfrom #Or if single bowl
frameresponse.LeftTo=scaleleftto
frameresponse.LeftDelta=scaleleftdiff
frameresponse.RightFrom=scalerightfrom
frameresponse.RightTo=scalerightto
frameresponse.RightDelta=scalerightdiff
#Return bowl count
if value[15] in range(4, 8):
tag="Manual"
frameresponse.Animal="Manual"
else:
tag = bytestotag(value[8:15])
curs.execute('select name from pets where tag=(?)', ([tag]))
petval = curs.fetchone()
if (petval):
frameresponse.Animal=petval.name
#Update weight values in database for known pet when the feeder closes
if value[15] == 1:
scalediff='['+scaleleftdiff+','+scalerightdiff+']'
updatedbtag('petstate',tag,device,'state', scalediff)
updatedbtag('petstate',tag,device,'timestamp', localtimestampfromnow())
else:
frameresponse.Animal=tag
#Update weight values in database for feeder when the feeder closes
if value[15] in [1,5]:
updatedb('feeders',device,'bowl1', scaleleftto)
updatedb('feeders',device,'bowl2', scalerightto)
curs.execute('select bowltype from feeders where mac_address=(?)', ([device]))
bowlval = curs.fetchone()
if (bowlval):
frameresponse.BowlCount=bowlval.bowltype
else:
frameresponse.BowlCount=1
else:
frameresponse.Operation="Unknown"
frameresponse.MSG=tohex(value)
elif value[0] == 0x1B: #Felaqua Drinking frame, similar to a feeder frame but slightly different
frameresponse.Operation="Drinking"
#Different operation values I assume
drinkaction=hb(value[8]) #Action performed
drinktime=b2iu(value[9:11]) #Time spent
drinkfrom=b2ih(value[12:16]) #Weight From
drinkto=b2ih(value[16:20]) #Weight To
drinkdiff=str(float(drinkto)-float(drinkfrom))
frameresponse.Action=drinkaction
frameresponse.Time=drinktime
frameresponse.From=drinkfrom
frameresponse.To=drinkto
frameresponse.Diff=drinkdiff
frameresponse.MSG=tohex(value)
#Update current weight value in database
upd = "UPDATE feeders SET bowl1=" + drinkto + ' WHERE mac_address = "' + device + '"'
curs.execute(upd)
conn.commit()
if len(value) > 27: #Includes the chip that performed the action
tag = bytestotag(value[27:34])
print("Hex tag",tohex(value[27:34]))
curs.execute('select name from pets where tag=(?)', ([tag]))
petval = curs.fetchone()
if (petval):
frameresponse.Animal=petval.name
else:
frameresponse.Animal=tag
else:
frameresponse.Operation="Unknown"
frameresponse.MSG=tohex(value)
return frameresponse
#Parse Hub Frames aka 132's sent to the hub
def parsehubframe(mac_address,offset,value):
response = []
frameresponse = Box()
message=bytearray.fromhex(value)
frameresponse.Offset = offset
frameresponse.MSG = value[2:]
if PrintHubFrame:
print("Hub Frame: MAC Address: " + mac_address + " offset " + str(offset) + " -value- " + str(value))
if offset == 15: #Adoption Mode
opvalue = str(int(message[1]))
operation="Adopt"
frameresponse.Operation=operation
frameresponse[operation]=HubAdoption(int(opvalue)).name
#sqlcmd('UPDATE hubs SET pairing_mode=' + opvalue + ' WHERE mac_address = "' + mac_address + '"')
elif offset == 18: #LED Mode
opvalue = str(int(message[1]))
operation="LED"
frameresponse.Operation=operation
frameresponse[operation]=HubLeds(int(opvalue)).name
sqlcmd('UPDATE hubs SET led_mode=' + opvalue + ' WHERE mac_address = "' + mac_address + '"')
else:
if message[0] >= 4: #This is a register dump message
curs.execute("INSERT OR REPLACE INTO devicestate values((?), (?), (?), (?));", (mac_address, offset, message[0], value[2:]))
conn.commit()
operation="Boot"
else:
operation="Other"
frameresponse.Operation=operation
#frameresponse[operation]=operation
response.append(frameresponse)
response.append({"Operation":operation})
return response
def parse132frame(mac_address,offset,value):
#Feeder and Cat Flap sends a 132 Status messages and most probably Felaqua sends one too but they only have a 33 type for the time and battery. I think these 132 frames are calcuated by the Hub as part of the RSSI frame.
response = []
frameresponse = Box();
message=bytearray.fromhex(value)
if offset == 33: #Battery and Door Time
operation="Data132Battery"
frameresponse.Operation=operation
#Battery ADC Calculation, Battery full 0xbd, and dies at 0x61/0x5f.
#ADC Start for Pet Door, not sure if this is consistent or just my door
adcstart=2.1075
#ADC Step value for each increment of the adc value
adcstep=0.0225
battadc = (int(message[1])*adcstep)+adcstart
frameresponse.Battery=str(battadc)
frameresponse.Time=converttime(message[2:4])
else:
operation="Other"
frameresponse.Operation=operation
frameresponse.MSG=tohex(value)
frameresponse[operation]=operation
response.append(frameresponse)
response.append({"Operation":[operation]})
return response
#Parse Pet Door Frames aka 132's sent to the pet door
def parsedoorframe(mac_address,offset,length,value):
response = []
operation = []
frameresponse = Box()
message=bytearray.fromhex(value)
frameresponse.MSG = value
messagerange = range(offset,offset+length)
if PrintFrameDbg:
print("Operation: " + str(operation) + " mac_address " + str(mac_address) + " offset " + str(offset) + " -value- " + str(value))
logmsg=""
if all(x in messagerange for x in [33]): #Battery and Door Time
print("Register 33 - Battery",messagerange)
operation.append("Battery")
#Battery ADC Calculation, Battery full 0xbd, and dies at 0x61/0x5f.
#ADC Start for Pet Door, not sure if this is consistent or just my door
adcstart=2.1075
#ADC Step value for each increment of the adc value
adcstep=0.0225
battadc = (int(message[33-offset])*adcstep)+adcstart
frameresponse.Battery=str(battadc)
frameresponse.BatteryADC=str(int(message[33-offset]))
sqlcmd('UPDATE devices SET battery=' + str(battadc) + ' WHERE mac_address = "' + mac_address + '"')
if all(x in messagerange for x in [34,35]): #Set local time for Pet Door 34 = HH in hex and 35 = MM
print("Register 34 - Time",messagerange)
operation.append("SetTime")
frameresponse.Time=converttime(message[34-offset:34-offset+2])
frameresponse.TimeMins=converttimetominutes(message[34-offset:34-offset+2])
if all(x in messagerange for x in [36]): #Lock state
print("Register 36 - Lockstate",messagerange)
operation.append("LockState")
frameresponse.LockStateNumber=message[36-offset]
frameresponse.LockState=PetDoorLockState(int(frameresponse.LockStateNumber)).name
sqlcmd('UPDATE doors SET lockingmode='+ str(frameresponse.LockStateNumber) +' WHERE mac_address = "' + mac_address + '"')
if all(x in messagerange for x in [40]): #Keep pets out to allow pets to come in state
print("Register 40 - LockedOutState",messagerange)
operation.append("LockedOutState")
frameresponse.LockedOut=PetDoorLockedOutState(int(message[40-offset])).name
if all(x in messagerange for x in [59]): #Provisioned Chip Count
print("Register 59 - Provchip",messagerange)
operation.append("ProvChipCount")
frameresponse.ChipCount=message[59-offset]
if all(x in messagerange for x in [60]): # Next free chip slot
print("Register 60 - next chip",messagerange)
operation.append("ProvFreeSlot")
frameresponse.ChipSlot = message[60-offset]
if offset in range(91,309): #Provisioned chips
print("Register 91-309 - provisioned chips",messagerange)
op="ProvChip"
frameresponse.Operation=op
operation.append(op)
pet=round((int(offset)-84)/7)-1 #Calculate the pet number
chip=doorhextochip(value[4:]) #Calculate chip Number
frameresponse.PetOffset=pet
frameresponse.Chip=chip
if offset == 519: #Curfew
print("Register 519 - curfew",messagerange)
op="Curfew"
frameresponse.Operation=op
operation.append(op)
frameresponse.CurfewState=CurfewState(message[0]).name
frameresponse.CurfewStateNumber=message[0]
frameresponse.Curfews=str(message[1]).zfill(2)+":"+str(message[2]).zfill(2)+'-'+str(message[3]).zfill(2)+":"+str(message[4]).zfill(2)
sqlcmd('UPDATE doors SET curfewenabled='+ str(message[0]) +' , curfews="'+frameresponse.Curfews+'" WHERE mac_address = "' + mac_address + '"')
if offset in range(525, 618): #Pet movement state in or out
op="PetMovement"
frameresponse.Operation=op
operation.append(op)
deviceindex=round((int(offset)-522)/3)-1 #Calculate the pet number
petstate = message[2]
if petstate > 0:
if PetDoorDirection.has_value(petstate):
direction = PetDoorDirection(petstate).name
else:
direction = "Other " + hb(petstate)
frameresponse.PetOffset=deviceindex
#Find Pet
curs.execute('select tag from tagmap where mac_address=(?) and deviceindex=(?)', (mac_address, deviceindex))
tagval = curs.fetchone()
if (tagval):
curs.execute('select name from pets where tag=(?)', ([tagval.tag]))
petval = curs.fetchone()
if (petval):
petname=petval.name
if petstate in [0x61, 0x81]:
petstate = "1" #Inside
else:
petstate = "0" #Otherwise Outside
updatedbtag('petstate',tagval.tag,mac_address,'state', petstate ) # Update state as inside or outside
updatedbtag('petstate',tagval.tag,mac_address,'timestamp', localtimestampfromnow()) #Update timestamp
else:
petname="Unknown"
else:
petname="Unknown"
frameresponse.Animal=petname
frameresponse.Direction=direction
else:
frameresponse.Animal = "Empty"
frameresponse.Direction = "Null"
operation.append("PetMovement")
if offset == 621: #Unknown pet went outside, should probably do a lookup to see what animals are still inside and update who is left??
op="PetMovement"
frameresponse.Operation=op
operation.append(op)
frameresponse.PetOffset="621"
frameresponse.Animal="UnknownPet"
frameresponse.Direction="Outside"
frameresponse.State="OFF"
#else:
# op="Other"
# frameresponse.Operation=op
# operation.append(op)
response.append(frameresponse)
response.append({"Operation":operation})
return response
def inithubmqtt():
response = Box();
#Devices
curs.execute('select name,product_id,devices.mac_address,serial_number,uptime,version,devices.custommode,state,battery,led_mode,pairing_mode,lockingmode,curfewenabled,curfews,bowl1,bowl2,bowltarget1,bowltarget2,bowltype,close_delay from devices left outer join hubs on devices.mac_address=hubs.mac_address left outer join doors on devices.mac_address=doors.mac_address left outer join feeders on devices.mac_address=feeders.mac_address;')
devices = curs.fetchall()
if devices:
response.devices = devices
#Pets
curs.execute('select pets.name as name,species,devices.name as device,product_id,state from pets left outer join petstate on pets.tag=petstate.tag left outer join devices on petstate.mac_address=devices.mac_address;')
pets = curs.fetchall()
if pets:
response.pets = pets
return response
def decodehubmqtt(topic,message):
response = Box();
msgsplit=message.split()
topicsplit=topic.split('/')
mac_address=topicsplit[-1]
#Decode device name
if mac_address=="messages":
curs.execute('select name,mac_address,product_id from devices where product_id=1')
devicename = curs.fetchone()
if devicename:
response.device = str(devicename.name)
response.mac_address = str(devicename.mac_address)
mac_address = devicename.mac_address
try:
int(msgsplit[0], 16)
timestampstr = str(datetime.utcfromtimestamp(int(msgsplit[0],16)))
except ValueError:
timestampstr = str(datetime.utcnow().replace(microsecond=0))
else:
curs.execute('select name,mac_address,product_id from devices where mac_address=(?)', ([mac_address]))
devicename = curs.fetchone()
if devicename:
response.device = str(devicename.name)
response.mac_address = mac_address
else:
response.device = str(mac_address)
response.mac_address = mac_address
timestampstr = str(datetime.utcfromtimestamp(int(msgsplit[0],16)))
response.message = message
response.timestamp=timestampstr
#Determine operation
if msgsplit[1] == "1000":
operation = "Command"
else:
operation = "Status"
response.operation = operation
resp = []
frameresponse = Box()
#Device message
if msgsplit[0] == "Hub": #Hub Offline Last Will message
op="State"
frameresponse.Operation=op
frameresponse.MSG=message
frameresponse[op]='Offline'
resp.append(frameresponse)
resp.append({"Operation":[op]})
#Update state in database
sqlcmd('UPDATE hubs SET state=0 WHERE mac_address = "' + mac_address + '"')
response.message = resp
elif msgsplit[2] == "Hub": #Hub online message
op="State"
frameresponse.Operation=op
frameresponse.MSG=message
frameresponse[op]='Online'
resp.append(frameresponse)
resp.append({"Operation":[op]})
#Update state in database
sqlcmd('UPDATE hubs SET state=1 WHERE mac_address = "' + mac_address + '"')
response.message = resp
elif msgsplit[2] == "10": #Hub Uptime
op="Uptime"
uptime = str(int(msgsplit[3]))
frameresponse.Operation=op
frameresponse[op]=uptime
frameresponse.TS=msgsplit[4]+"-"+':'.join(format(int(x), '02d') for x in msgsplit[5:8])
frameresponse.Reconnect=msgsplit[9]
resp.append(frameresponse)
resp.append({"Operation":[op]})
#Update uptime in database
sqlcmd('UPDATE hubs SET Uptime=' + uptime + ' WHERE mac_address = "' + mac_address + '"')
response.message = resp
elif msgsplit[2] == "132" and EntityType(int(devicename.product_id)).name == "HUB": #Hub Frame
if PrintHubFrame:
print("Hub Message : "+message)
msgsplit[5] = hb(int(msgsplit[5])) #Convert length at offset 5 which is decimal into hex byte so we pass it as a hex string to parsedataframe
response.message = parsehubframe(mac_address,int(msgsplit[4]),"".join(msgsplit[5:]))
elif msgsplit[2] == "132" and EntityType(int(devicename.product_id)).name == "PETDOOR": #Pet Door Status
#Status message has a counter at offset 4 we can ignore:
if Print132Frame:
print("132 Message : "+message)
msgsplit[5] = hb(int(msgsplit[5])) #Convert length at offset 5 which is decimal into hex byte so we pass it as a hex string to parsedataframe
response.message = parsedoorframe(mac_address, int(msgsplit[4]),int(msgsplit[5]),"".join(msgsplit[6:]))
elif msgsplit[2] == "132": #Feeder 132 Status
#Status message has a counter at offset 4 we can ignore:
if PrintFeederFrame:
print("NonHub/PetDoor 132 Message : "+message)
msgsplit[5] = hb(int(msgsplit[5])) #Convert length at offset 5 which is decimal into hex byte so we pass it as a hex string to parsedataframe
#print("Message :", "".join(msgsplit[5:]))
response.message = parse132frame(mac_address, int(msgsplit[4]),"".join(msgsplit[5:]))
elif msgsplit[2] == "127": #127 Feeder/CatDoor frame sent/control message
singleframe = bytearray.fromhex("".join(msgsplit[3:]))
singleresponse = []
singleframeresponse = parseframe(mac_address, singleframe)
singleresponse.append(singleframeresponse)
singleframeop=singleframeresponse.Operation
singleresponse.append({"Operation":[singleframeop]})
response.message = singleresponse
elif msgsplit[2] == "126": #126 Feeder/CatDoor multiframe status message
multiframe = bytearray.fromhex("".join(msgsplit[3:]))
response.message = parsemultiframe(mac_address,multiframe)
elif msgsplit[2] == "2" and EntityType(int(devicename.product_id)).name == "HUB": #Action message setting value to Hub Pet Door
#Action message doesn't have a counter
if Print2Frame:
print("2 Message : "+message)
msgsplit[4] = hb(int(msgsplit[4])) #Convert length at offset 4 which is decimal into hex byte so we pass it as a hex string to parsedoorframe
response.message = parsehubframe(mac_address, int(msgsplit[3]),"".join(msgsplit[4:]))
elif msgsplit[2] == "2" and EntityType(int(devicename.product_id)).name == "PETDOOR": #Action message setting value to Hub Pet Door
#Action message doesn't have a counter
if Print2Frame:
print("2 Message : "+message)
msgsplit[4] = hb(int(msgsplit[4])) #Convert length at offset 4 which is decimal into hex byte so we pass it as a hex string to parsedoorframe
response.message = parsedoorframe(mac_address, int(msgsplit[3]),int(msgsplit[4]),"".join(msgsplit[5:]))
elif msgsplit[2] == "8": #Action message setting value to Pet Door
resp.append({"Msg":message})
resp.append({"Operation":["8"]})
response.message = resp
elif msgsplit[2] == "3": # Boot message - dump memory
resp.append({"Msg":"Dump to " + msgsplit[4]})
resp.append({"Operation":["Dump"]})
response.message = resp
else:
resp.append({"Msg":message})
resp.append({"Operation":["ERROR"]})
response.message = resp
return Box(response)
def buildmqttsendmessage(value):
return hextimestampfromnow() + " 1000 " + value
#Generate message
def generatemessage(mac_address,operation,state):
if PrintDebug:
print("GenerateMessage: Mac={} Op={} State={}".format(mac_address,operation,state))
curs.execute('select product_id from devices where mac_address like (?)', ([mac_address]))
device = curs.fetchone()
if PrintDebug:
print("Device: ",EntityType(int(device.product_id)).name)
if EntityType(int(device.product_id)).name == "HUB": #Hub
operations = Box({
"DumpState" : { "msg" : "3 0 205", "desc" : "Dump current configuration" }, #Dump all memory registers from 0 to 205
"EarsOff" : { "msg" : "2 18 1 00", "desc" : "Ears off" }, #Ears off state
"EarsOn" : { "msg" : "2 18 1 01", "desc" : "Ears on" }, #Ears on state
"EarsDimmed" : { "msg" : "2 18 1 04", "desc" : "Ears dimmed" }, #Ears dimmed state
"FlashEarsOff" : { "msg" : "2 18 1 80", "desc" : "Flash ears 3 times and return to ears off" }, #Flash the ears 3 times, return to off state
"FlashEarsOn" : { "msg" : "2 18 1 81", "desc" : "Flash ears 3 times and return to ears on" }, #Flash the ears 3 times, return to on state
"FlashEarsDim" : { "msg" : "2 18 1 84", "desc" : "Flash ears 3 times and return to ears dimmed" }, #Flash the ears 3 times, return to dimmed state
"AdoptEnable" : { "msg" : "2 15 1 02", "desc" : "Enable adoption mode to adopt devices." }, #Enable adoption mode to adopt new devices
"AdoptDisable" : { "msg" : "2 15 1 00", "desc" : "Disable adoption mode" }, #Disable adoption mode
"AdoptButton" : { "msg" : "2 15 1 82", "desc" : "Enable adoption using reset button." }, #Enable adoption mode as if you pressed the button under the hub
"RemoveDev0" : { "msg" : "2 22 1 00", "desc" : "Remove Provisioned device 0" }, #Remove Provisioned device 0
"RemoveDev1" : { "msg" : "2 22 1 01", "desc" : "Remove Provisioned device 1" }, #Remove Provisioned device 1
"RemoveDev2" : { "msg" : "2 22 1 02", "desc" : "Remove Provisioned device 2" }, #Remove Provisioned device 2
"RemoveDev3" : { "msg" : "2 22 1 03", "desc" : "Remove Provisioned device 3" }, #Remove Provisioned device 3
"RemoveDev4" : { "msg" : "2 22 1 04", "desc" : "Remove Provisioned device 4" } #Remove Provisioned device 4
})
if operation == "operations":
return operations
elif operation in operations:
#print("Operation to do: " + operation)
return Box({"topic":"pethublocal/messages", "msg":buildmqttsendmessage(operations[operation].msg)})
elif operation == "custom":
return Box({"topic":"pethublocal/messages", "msg":buildmqttsendmessage(state)})
else:
return Box({"error":"Unknown message"})
elif EntityType(int(device.product_id)).name == "PETDOOR": #Pet Door
curfewstate = Box({
"OFF" : "01", #Disable Curfew State
"ON" : "02" #Enable Curfew State
})
lockstate = Box({
"Unlocked": "00", # Unlocked
"LockKeepIn": "01", # Keep Pets in
"LockKeepOut": "02", # Keep Pets out
"Locked": "03", # Locked both ways
"CurfewMode": "04" #Curfew mode enabled
})
operations = Box({
"DumpState" : { "msg" : "3 0 630", "desc" : "Dump current registers" }, #Dump all memory registers from 0 to 630
"GetBattery" : { "msg" : "3 30 10", "desc" : "Get Battery" }, #Get Battery State
"GetProv" : { "msg" : "3 59 2", "desc" : "Get Prov Tags" }, #Get Prov Tags
"GetSlot" : { "msg" : "3 60 1", "desc" : "Get Prov Tags" }, #Get Prov Tags
"GetTag" : { "msg": "3 91 35", "desc": "Get Prov Tags"}, # Get Prov Tags
"SetTime" : { "msg" : "2 34 2 HH MM", "desc" : "Set the time" }, #Set the time on the pet door HH MM in hex
"CustomMode" : { "msg" : "2 61 3 00 00 00", "desc" : "Set Custom mode" }, #Set custom mode as a bit operator
"LockState" : { "msg" : "2 36 1 SS", "desc" : "Set Lock State" }, #Set Lock State
"Unlocked" : { "msg" : "2 36 1 00", "desc" : "Unlocked" }, #Unlocked
"LockKeepIn" : { "msg" : "2 36 1 01", "desc" : "Keep pets in" }, #Keep Pets in
"LockKeepOut" : { "msg" : "2 36 1 02", "desc" : "Keep pets out" }, #Keep Pets out
"Locked" : { "msg" : "2 36 1 03", "desc" : "Locked both way" }, #Locked both ways
"CurfewMode" : { "msg" : "2 36 1 04", "desc" : "Curfew enabled" }, #Curfew mode enabled
"LockState39" : { "msg" : "2 39 1 01", "desc" : "Lock State 39" }, #Not sure if this is needed, but it was set once during set locking state.
"CurfewState" : { "msg" : "2 519 6 SS FF FF TT TT 00", "desc" : "Set Curfew time From / To" }, #Enable curfew time from database
})
if operation in operations:
message = operations[operation].msg
#Set the time
now = datetime.now() # Current timestamp
message = message.replace('HH MM', hb(now.hour)+" "+hb(now.minute)) #Set the time in hex
elif operation == "KeepIn" or operation == "KeepOut":
curs.execute('select lockingmode from doors where mac_address = (?)', ([mac_address]))
lmresp = curs.fetchone()
lm = lmresp.lockingmode
if PrintDebug:
print("Locking mode in database: ", lm)
if (operation == "KeepIn" and state == "OFF" and lm == 1) or (operation == "KeepOut" and state == "OFF" and lm == 2): #Going to Lock State 0 - Unlocked
message = operations["Unlocked"].msg
elif (operation == "KeepIn" and state == "ON" and lm == 0) or (operation == "KeepOut" and state == "OFF" and lm == 3): #Going to Lock State 1 - Keep pets in
message = operations["LockKeepIn"].msg
elif (operation == "KeepIn" and state == "OFF" and lm == 3) or (operation == "KeepOut" and state == "ON" and lm == 0): #Going to Lock State 2 - Keep pets out
message = operations["LockKeepOut"].msg
elif (operation == "KeepIn" and state == "ON" and lm == 2) or (operation == "KeepOut" and state == "ON" and lm == 1): #Going to Lock State 3 - Lock both ways
message = operations["Locked"].msg
else:
message = operations["Unlocked"].msg
elif operation == "CurfewLock":
if (state == "ON"): #Curfew lock state 4
message = operations["CurfewMode"].msg
else: #Going to Lock State 0 - Unlocked
message = operations["Unlocked"].msg
elif operation == "SetCurfewState": #Curfew, EE = Enable State, FF = From HH:MM, TT = To HH:MM
message = operations['CurfewState'].msg
curs.execute('select curfewenabled,curfews from doors where mac_address = (?)', ([mac_address]))
curfew = curs.fetchone()
#print("Current curfew mode: ", curfew.curfewenabled)
curfewsstartstop = curfew.curfews.split('-')
message = message.replace('FF FF TT TT', converttimetohex(curfewsstartstop[0]) + " " + converttimetohex(curfewsstartstop[1])) #Set the curfew time
if state == 'UPD':
message = message.replace("SS", hb(int(curfew.curfewenabled)))
elif state in curfewstate: #Has string value to map
message = message.replace("SS", curfewstate[state])
elif hb(int(state)) in curfewstate.values(): #Has value that exists in validation dictionary
message = message.replace("SS", hb(int(state)))
else:
return Box({"error":"Unknown message"})
else:
return Box({"error":"Unknown message"})
return Box({"topic":"pethublocal/messages/"+mac_address, "msg":buildmqttsendmessage(message)})
elif EntityType(int(device.product_id)).name == "FEEDER": #Feeder
ackdatatype = Box({
"Config" : "09", #Config registers
"Unknown0b" : "0b", #Unknown 0b message
"Battery" : "0c", #Battery state change
"Boot10" : "10", #Boot message 10
"Tags" : "11", #Tag provisioning
"Custom" : "14", #Tag provisioning
"Status16" : "16", #Status 16 message, happens each time feeder manually opened
"Boot17" : "17", #Boot message 17
"Feeder" : "18", #Feeder state change
})
getdatatype = Box({
"Time" : "07 00", #Time
"Config" : "09 00 ff", #Config registers
"Unknown0b" : "0b 00", #Unknown 0b
"Battery" : "0c 00", #Battery state
"Boot10" : "10 00", #Boot message 10
"Tags" : "11 00 ff", #Tag provisioned
"Boot17" : "17 00 00", #Boot message 17
})
bowlcount = Box({
"One" : "01", #One bowl
"Two" : "02" #Two bowls
})
lidclosedelay = Box({
"Fast" : "00 00 00 00", #0 Seconds
"Normal" : "a0 0f 00 00", #4 Seconds "0fa0" = 4000
"Slow" : "20 4e 00 00" #20 Seconds "4e20" = 20000
})
zeroscale = Box({
"Left" : "01", #Zero left scale
"Right" : "02", #Zero right scale
"Both" : "03" #Zero both scale
})
#All messages detected sending to the feeder, if the fields have validation then they have a validate date referencing the above dictionary key value pairs
operations = Box({
"Ack" : { "msg" : "127 00 00 ZZ ZZ TT TT TT TT SS 00 00", "desc" : "Send acknowledge to data type", "validate": ackdatatype }, #Send acknowledge to data type
"Get" : { "msg" : "127 01 00 ZZ ZZ TT TT TT TT SS", "desc" : "Get current state of data type", "validate": getdatatype }, #Get data type state
"SetTime" : { "msg" : "127 07 00 ZZ ZZ TT TT TT TT 00 00 00 00 07", "desc" : "Set the device time" }, #Set device time, seems like the last byte = 04 sets time when going forward, 05 sets time, 06 sets time on boot
"SetLeftScale" : { "msg" : "127 09 00 ZZ ZZ TT TT TT TT 0a WW WW WW WW", "desc" : "Set the left or single scale target weight" }, #Set left or single scale weight in grams to 2 decimal places
"SetRightScale" : { "msg" : "127 09 00 ZZ ZZ TT TT TT TT 0b WW WW WW WW", "desc" : "Set the right scale target weight" }, #Set right scale weight in grams to 2 decimal places
"SetBowlCount" : { "msg" : "127 09 00 ZZ ZZ TT TT TT TT 0c SS 00 00 00", "desc" : "Set the bowl count", "validate": bowlcount }, #Set bowl count either 01 for one bowl or 02 for two.
"SetCloseDelay" : { "msg" : "127 09 00 ZZ ZZ TT TT TT TT 0d LL LL LL LL", "desc" : "Set the lid close delay" }, #Set lid close delay, 0 (fast) , 4 seconds (normal), 20 seconds (slow)
"Set12" : { "msg" : "127 09 00 ZZ ZZ TT TT TT TT 12 f4 01 00 00", "desc" : "Set the 12 message" }, #Not sure what caused this but it happened around setting the scales
"Custom" : { "msg" : "127 09 00 ZZ ZZ TT TT TT TT 14 CM CM CM CM", "desc" : "Set Custom Mode" }, #Custom mode
"Custom-Intruder" : { "msg" : "127 09 00 ZZ ZZ TT TT TT TT 14 00 01 00 00", "desc" : "Set Custom Mode - Intruder" }, #Custom mode - Intruder
"Custom-GeniusCat" : { "msg" : "127 09 00 ZZ ZZ TT TT TT TT 14 80 00 00 00", "desc" : "Set Custom Mode - Genius Cat Mode"}, #Custom mode - Genius Cat Mode
"ZeroScale" : { "msg" : "127 0d 00 ZZ ZZ TT TT TT TT 00 19 00 00 00 03 00 00 00 00 01 SS", "desc" : "Zero the scales left/right/both", "validate": zeroscale }, #Zero left right or both scales
"TagProvision" : { "msg" : "127 11 00 ZZ ZZ TT TT TT TT CC CC CC CC CC CC CC 02 II SS", "desc" : "Provision/enable or disable chip" } #Provision or enable or disable chip
})
if operation == 'Ack':
message = operations[operation].msg
inack = state.split('-')
message = message.replace("SS", hb(int(inack[0], 16)))
message = message.replace('ZZ ZZ', splitbyte(int(inack[1]).to_bytes(2, 'little').hex())) # Replace device send counter in the record
hubts = splitbyte(devicetimestampfromnow())
message = message.replace('TT TT TT TT', hubts) #Replace timestamp in the record
return Box({"topic":"pethublocal/messages/"+mac_address, "msg":buildmqttsendmessage(message)})
elif operation in operations:
message = operations[operation].msg
#Update standard values of the counter, and the timestamp
hubts = splitbyte(devicetimestampfromnow())
devcount = devicecounter(mac_address,"-1","-2") #Iterate the send counter for the device