maxcube-client 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/.rubocop.yml +32 -0
- data/Gemfile +5 -0
- data/LICENSE.md +21 -0
- data/README.md +35 -0
- data/Rakefile +6 -0
- data/bin/console +8 -0
- data/bin/maxcube-client +31 -0
- data/bin/sample_server +13 -0
- data/bin/sample_socket +13 -0
- data/bin/setup +6 -0
- data/data/load/del +6 -0
- data/data/load/meta +20 -0
- data/data/load/ntp +6 -0
- data/data/load/set_temp +13 -0
- data/data/load/set_temp_mode +12 -0
- data/data/load/set_valve +11 -0
- data/data/load/url +4 -0
- data/data/load/wake +4 -0
- data/lib/maxcube/messages.rb +148 -0
- data/lib/maxcube/messages/handler.rb +154 -0
- data/lib/maxcube/messages/parser.rb +34 -0
- data/lib/maxcube/messages/serializer.rb +59 -0
- data/lib/maxcube/messages/tcp.rb +18 -0
- data/lib/maxcube/messages/tcp/handler.rb +70 -0
- data/lib/maxcube/messages/tcp/parser.rb +46 -0
- data/lib/maxcube/messages/tcp/serializer.rb +47 -0
- data/lib/maxcube/messages/tcp/type/a.rb +32 -0
- data/lib/maxcube/messages/tcp/type/c.rb +248 -0
- data/lib/maxcube/messages/tcp/type/f.rb +33 -0
- data/lib/maxcube/messages/tcp/type/h.rb +70 -0
- data/lib/maxcube/messages/tcp/type/l.rb +131 -0
- data/lib/maxcube/messages/tcp/type/m.rb +185 -0
- data/lib/maxcube/messages/tcp/type/n.rb +44 -0
- data/lib/maxcube/messages/tcp/type/q.rb +18 -0
- data/lib/maxcube/messages/tcp/type/s.rb +246 -0
- data/lib/maxcube/messages/tcp/type/t.rb +38 -0
- data/lib/maxcube/messages/tcp/type/u.rb +19 -0
- data/lib/maxcube/messages/tcp/type/z.rb +36 -0
- data/lib/maxcube/messages/udp.rb +9 -0
- data/lib/maxcube/messages/udp/handler.rb +40 -0
- data/lib/maxcube/messages/udp/parser.rb +50 -0
- data/lib/maxcube/messages/udp/serializer.rb +30 -0
- data/lib/maxcube/messages/udp/type/h.rb +24 -0
- data/lib/maxcube/messages/udp/type/i.rb +23 -0
- data/lib/maxcube/messages/udp/type/n.rb +21 -0
- data/lib/maxcube/network.rb +14 -0
- data/lib/maxcube/network/tcp.rb +11 -0
- data/lib/maxcube/network/tcp/client.rb +174 -0
- data/lib/maxcube/network/tcp/client/commands.rb +286 -0
- data/lib/maxcube/network/tcp/sample_server.rb +96 -0
- data/lib/maxcube/network/udp.rb +11 -0
- data/lib/maxcube/network/udp/client.rb +52 -0
- data/lib/maxcube/network/udp/sample_socket.rb +65 -0
- data/lib/maxcube/version.rb +4 -0
- data/maxcube-client.gemspec +29 -0
- metadata +155 -0
@@ -0,0 +1,33 @@
|
|
1
|
+
|
2
|
+
module MaxCube
|
3
|
+
module Messages
|
4
|
+
module TCP
|
5
|
+
class Parser
|
6
|
+
module MessageF
|
7
|
+
private
|
8
|
+
|
9
|
+
KEYS = %i[ntp_servers].freeze
|
10
|
+
|
11
|
+
# NTP server message
|
12
|
+
def parse_tcp_f(body)
|
13
|
+
{ ntp_servers: body.split(',') }
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
class Serializer
|
19
|
+
module MessageF
|
20
|
+
private
|
21
|
+
|
22
|
+
OPT_KEYS = %i[ntp_servers].freeze
|
23
|
+
|
24
|
+
# Request for NTP servers message (F)
|
25
|
+
# Optionally, updates can be done
|
26
|
+
def serialize_tcp_f(hash)
|
27
|
+
hash.key?(:ntp_servers) ? hash[:ntp_servers].join(',') : ''
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
|
2
|
+
module MaxCube
|
3
|
+
module Messages
|
4
|
+
module TCP
|
5
|
+
class Parser
|
6
|
+
module MessageH
|
7
|
+
private
|
8
|
+
|
9
|
+
LENGTHS = [10, 6, 4, 8, 8, 2, 2, 6, 4, 2, 4].freeze
|
10
|
+
|
11
|
+
KEYS = %i[
|
12
|
+
serial_number
|
13
|
+
rf_address
|
14
|
+
firmware_version
|
15
|
+
unknown
|
16
|
+
http_id
|
17
|
+
duty_cycle
|
18
|
+
free_memory_slots
|
19
|
+
cube_datetime
|
20
|
+
state_cube_time
|
21
|
+
ntp_counter
|
22
|
+
].freeze
|
23
|
+
|
24
|
+
# Hello message
|
25
|
+
def parse_tcp_h(body)
|
26
|
+
values = body.split(',')
|
27
|
+
check_msg_part_lengths(LENGTHS, *values)
|
28
|
+
values[1], _, values[4], values[5], values[6], _, _,
|
29
|
+
values[9], values[10] =
|
30
|
+
to_ints(16, 'RF address, ' \
|
31
|
+
'firmware version, ' \
|
32
|
+
'HTTP connection ID, ' \
|
33
|
+
'duty cycle, ' \
|
34
|
+
'free memory slots, ' \
|
35
|
+
'Cube date, ' \
|
36
|
+
'Cube time, ' \
|
37
|
+
'state Cube time (clock set), ' \
|
38
|
+
'NTP counter',
|
39
|
+
values[1], values[2], values[4], values[5], values[6],
|
40
|
+
values[7], values[8], values[9], values[10])
|
41
|
+
|
42
|
+
parse_tcp_h_cube_datetime(values)
|
43
|
+
|
44
|
+
KEYS.zip(values).to_h
|
45
|
+
end
|
46
|
+
|
47
|
+
########################
|
48
|
+
|
49
|
+
def parse_tcp_h_cube_datetime(values)
|
50
|
+
date, time = values[7..8]
|
51
|
+
year = 2000 + date[0..1].to_i(16)
|
52
|
+
|
53
|
+
month = date[2..3].to_i(16)
|
54
|
+
day = date[4..5].to_i(16)
|
55
|
+
hours = time[0..1].to_i(16)
|
56
|
+
minutes = time[2..3].to_i(16)
|
57
|
+
|
58
|
+
values[7] = DateTime.new(year, month, day, hours, minutes)
|
59
|
+
values.delete_at(8)
|
60
|
+
rescue ArgumentError
|
61
|
+
raise InvalidMessageBody
|
62
|
+
.new(@msg_type, 'invalid datetime format (YYMMDD HHMM): ' \
|
63
|
+
"#{date} #{time} " \
|
64
|
+
"-> #{year}-#{month}-#{day} #{hours}:#{minutes}")
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,131 @@
|
|
1
|
+
|
2
|
+
module MaxCube
|
3
|
+
module Messages
|
4
|
+
module TCP
|
5
|
+
class Parser
|
6
|
+
module MessageL
|
7
|
+
private
|
8
|
+
|
9
|
+
LENGTHS = [6, 11, 12].freeze
|
10
|
+
|
11
|
+
KEYS = %i[devices].freeze
|
12
|
+
|
13
|
+
# Device list message
|
14
|
+
def parse_tcp_l(body)
|
15
|
+
@io = StringIO.new(decode(body), 'rb')
|
16
|
+
|
17
|
+
hash = { devices: [] }
|
18
|
+
until @io.eof?
|
19
|
+
subhash = parse_tcp_l_submsg_1
|
20
|
+
|
21
|
+
temperature_msb = parse_tcp_l_submsg_2(subhash) if @length > 6
|
22
|
+
parse_tcp_l_submsg_3(subhash, temperature_msb) if @length > 11
|
23
|
+
|
24
|
+
hash[:devices] << subhash
|
25
|
+
end # until
|
26
|
+
|
27
|
+
hash
|
28
|
+
end
|
29
|
+
|
30
|
+
########################
|
31
|
+
|
32
|
+
def parse_tcp_l_submsg_1
|
33
|
+
@length = read(1, true)
|
34
|
+
unless LENGTHS.include?(@length)
|
35
|
+
raise InvalidMessageBody
|
36
|
+
.new(@msg_type, "invalid length of submessage (#{@length}):" \
|
37
|
+
" should be in #{LENGTHS}")
|
38
|
+
end
|
39
|
+
subhash = {
|
40
|
+
length: @length,
|
41
|
+
rf_address: read(3, true),
|
42
|
+
unknown: read(1),
|
43
|
+
}
|
44
|
+
flags = read(2, true)
|
45
|
+
@mode = device_mode(flags & 0x3)
|
46
|
+
subhash[:flags] = {
|
47
|
+
value: flags,
|
48
|
+
mode: @mode,
|
49
|
+
dst_setting_active: !((flags & 0x8) >> 3).zero?,
|
50
|
+
gateway_known: !((flags & 0x10) >> 4).zero?,
|
51
|
+
panel_locked: !((flags & 0x20) >> 5).zero?,
|
52
|
+
link_error: !((flags & 0x40) >> 6).zero?,
|
53
|
+
low_battery: !((flags & 0x80) >> 7).zero?,
|
54
|
+
status_initialized: !((flags & 0x200) >> 9).zero?,
|
55
|
+
is_answer: !((flags & 0x400) >> 10).zero?,
|
56
|
+
error: !((flags & 0x800) >> 11).zero?,
|
57
|
+
valid_info: !((flags & 0x1000) >> 12).zero?,
|
58
|
+
}
|
59
|
+
|
60
|
+
subhash
|
61
|
+
rescue IOError
|
62
|
+
raise InvalidMessageBody
|
63
|
+
.new(@msg_type, 'unexpected EOF reached at submessage 1st part')
|
64
|
+
end
|
65
|
+
|
66
|
+
def parse_tcp_l_submsg_2(subhash)
|
67
|
+
subhash[:valve_opening] = read(1, true)
|
68
|
+
|
69
|
+
temperature = read(1, true)
|
70
|
+
# This bit may be used later
|
71
|
+
temperature_msb = temperature >> 7
|
72
|
+
subhash[:temperature] = (temperature & 0x3f).to_f / 2
|
73
|
+
|
74
|
+
date_until = read(2, true)
|
75
|
+
year = (date_until & 0x1f) + 2000
|
76
|
+
month = ((date_until & 0x80) >> 7) | ((date_until & 0xe000) >> 12)
|
77
|
+
day = (date_until & 0x1f00) >> 8
|
78
|
+
|
79
|
+
time_until = read(1, true)
|
80
|
+
hours = time_until / 2
|
81
|
+
minutes = (time_until % 2) * 30
|
82
|
+
# Sometimes when device is in 'auto' mode,
|
83
|
+
# this field can contain 'actual_temperature' instead
|
84
|
+
# (but never if it is already contained in next byte)
|
85
|
+
# !It seems that 'datetime' is used for 'vacation' mode,
|
86
|
+
# but it is not sure ...
|
87
|
+
begin
|
88
|
+
subhash[:datetime_until] = DateTime.new(year, month, day,
|
89
|
+
hours, minutes)
|
90
|
+
rescue ArgumentError
|
91
|
+
if @mode != :auto || @length > 11
|
92
|
+
raise InvalidMessageBody
|
93
|
+
.new(@msg_type, "unrecognized message part: #{date_until}" \
|
94
|
+
" (it does not seem to be 'date until'" \
|
95
|
+
" nor 'actual temperature')")
|
96
|
+
end
|
97
|
+
subhash[:actual_temperature] = date_until.to_f / 10
|
98
|
+
end
|
99
|
+
|
100
|
+
temperature_msb
|
101
|
+
rescue IOError
|
102
|
+
raise InvalidMessageBody
|
103
|
+
.new(@msg_type,
|
104
|
+
'unexpected EOF reached at submessage 2nd part')
|
105
|
+
end
|
106
|
+
|
107
|
+
def parse_tcp_l_submsg_3(subhash, temperature_msb)
|
108
|
+
subhash[:actual_temperature] = ((temperature_msb << 8) |
|
109
|
+
read(1, true)).to_f / 10
|
110
|
+
rescue IOError
|
111
|
+
raise InvalidMessageBody
|
112
|
+
.new(@msg_type,
|
113
|
+
'unexpected EOF reached at submessage 3rd part')
|
114
|
+
end
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
class Serializer
|
119
|
+
module MessageL
|
120
|
+
private
|
121
|
+
|
122
|
+
# Command to resend device list (L)
|
123
|
+
# Does not contain any data
|
124
|
+
def serialize_tcp_l(_hash)
|
125
|
+
''
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
@@ -0,0 +1,185 @@
|
|
1
|
+
|
2
|
+
module MaxCube
|
3
|
+
module Messages
|
4
|
+
module TCP
|
5
|
+
class Parser
|
6
|
+
module MessageM
|
7
|
+
private
|
8
|
+
|
9
|
+
LENGTHS = [2, 2].freeze
|
10
|
+
|
11
|
+
KEYS = %i[index count unknown1 unknown2
|
12
|
+
rooms_count rooms devices_count devices].freeze
|
13
|
+
|
14
|
+
# Metadata message
|
15
|
+
def parse_tcp_m(body)
|
16
|
+
index, count, enc_data = parse_tcp_m_split(body)
|
17
|
+
|
18
|
+
@io = StringIO.new(decode(enc_data), 'rb')
|
19
|
+
|
20
|
+
hash = { index: index, count: count, unknown1: read(2), }
|
21
|
+
parse_tcp_m_rooms(hash)
|
22
|
+
parse_tcp_m_devices(hash)
|
23
|
+
hash[:unknown2] = read(1)
|
24
|
+
|
25
|
+
hash
|
26
|
+
rescue IOError
|
27
|
+
raise InvalidMessageBody
|
28
|
+
.new(@msg_type,
|
29
|
+
'unexpected EOF reached at unknown parts' \
|
30
|
+
' of decoded message data')
|
31
|
+
end
|
32
|
+
|
33
|
+
########################
|
34
|
+
|
35
|
+
def parse_tcp_m_split(body)
|
36
|
+
index, count, enc_data = body.split(',')
|
37
|
+
check_msg_part_lengths(LENGTHS, index, count)
|
38
|
+
index, count = to_ints(16, 'message index, count',
|
39
|
+
index, count)
|
40
|
+
unless index < count
|
41
|
+
raise InvalidMessageBody
|
42
|
+
.new(@msg_type,
|
43
|
+
"index >= count: #{index} >= #{count}")
|
44
|
+
end
|
45
|
+
|
46
|
+
unless enc_data
|
47
|
+
raise InvalidMessageBody
|
48
|
+
.new(@msg_type, 'message data is missing')
|
49
|
+
end
|
50
|
+
|
51
|
+
[index, count, enc_data]
|
52
|
+
end
|
53
|
+
|
54
|
+
def parse_tcp_m_rooms(hash)
|
55
|
+
rooms_count = read(1, true)
|
56
|
+
hash[:rooms_count] = rooms_count
|
57
|
+
hash[:rooms] = []
|
58
|
+
rooms_count.times do
|
59
|
+
room_id = read(1, true)
|
60
|
+
room_name_length = read(1, true)
|
61
|
+
room = {
|
62
|
+
id: room_id,
|
63
|
+
name_length: room_name_length,
|
64
|
+
name: read(room_name_length),
|
65
|
+
rf_address: read(3, true)
|
66
|
+
}
|
67
|
+
|
68
|
+
# hash[:rooms][room_id] = room
|
69
|
+
hash[:rooms] << room
|
70
|
+
end
|
71
|
+
rescue IOError
|
72
|
+
raise InvalidMessageBody
|
73
|
+
.new(@msg_type,
|
74
|
+
'unexpected EOF reached at rooms data part' \
|
75
|
+
' of decoded message data')
|
76
|
+
end
|
77
|
+
|
78
|
+
def parse_tcp_m_devices(hash)
|
79
|
+
devices_count = read(1, true)
|
80
|
+
hash[:devices_count] = devices_count
|
81
|
+
hash[:devices] = []
|
82
|
+
devices_count.times do
|
83
|
+
device = {
|
84
|
+
type: device_type(read(1, true)),
|
85
|
+
rf_address: read(3, true),
|
86
|
+
serial_number: read(10),
|
87
|
+
}
|
88
|
+
device_name_length = read(1, true)
|
89
|
+
device.merge!(
|
90
|
+
name_length: device_name_length,
|
91
|
+
name: read(device_name_length),
|
92
|
+
room_id: read(1, true),
|
93
|
+
)
|
94
|
+
|
95
|
+
hash[:devices] << device
|
96
|
+
end
|
97
|
+
rescue IOError
|
98
|
+
raise InvalidMessageBody
|
99
|
+
.new(@msg_type,
|
100
|
+
'unexpected EOF reached at devices data part' \
|
101
|
+
' of decoded message data')
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
class Serializer
|
107
|
+
module MessageM
|
108
|
+
private
|
109
|
+
|
110
|
+
KEYS = %i[rooms_count rooms devices_count devices].freeze
|
111
|
+
OPT_KEYS = %i[index unknown1 unknown2].freeze
|
112
|
+
|
113
|
+
# Serialize metadata for Cube
|
114
|
+
# Message body has the same format as response (M)
|
115
|
+
# -> reverse operations
|
116
|
+
# ! I couldn't verify the assumption that bodies should be the same
|
117
|
+
# ! Cube does not check data format,
|
118
|
+
# so things could break if invalid data is sent
|
119
|
+
def serialize_tcp_m(hash)
|
120
|
+
index = hash.key?(:index) ? to_int(0, 'index', hash[:index]) : 0
|
121
|
+
head = format('%02x,', index)
|
122
|
+
|
123
|
+
@io = StringIO.new('', 'wb')
|
124
|
+
write(hash.key?(:unknown1) ? hash[:unknown1] : "\x00\x00")
|
125
|
+
|
126
|
+
serialize_tcp_m_rooms(hash)
|
127
|
+
serialize_tcp_m_devices(hash)
|
128
|
+
write(hash.key?(:unknown2) ? hash[:unknown2] : "\x00")
|
129
|
+
|
130
|
+
head.b << encode(@io.string)
|
131
|
+
end
|
132
|
+
|
133
|
+
########################
|
134
|
+
|
135
|
+
def serialize_tcp_m_rooms(hash)
|
136
|
+
write(to_int(0, 'rooms count', hash[:rooms_count]), esize: 1)
|
137
|
+
hash[:rooms].each do |room|
|
138
|
+
name = room[:name]
|
139
|
+
if room.key?(:name_length)
|
140
|
+
name_length = to_int(0, 'name length', room[:name_length])
|
141
|
+
unless name_length == name.length
|
142
|
+
raise InvalidMessageBody
|
143
|
+
.new(@msg_type, 'room name length and length of name' \
|
144
|
+
" mismatch: #{name_length} != #{name.length}")
|
145
|
+
end
|
146
|
+
else
|
147
|
+
name_length = name.length
|
148
|
+
end
|
149
|
+
|
150
|
+
id, rf_address = to_ints(0, 'room id, RF address',
|
151
|
+
room[:id], room[:rf_address])
|
152
|
+
write(serialize(id, name_length, name, esize: 1) <<
|
153
|
+
serialize(rf_address, esize: 3))
|
154
|
+
end
|
155
|
+
end
|
156
|
+
|
157
|
+
def serialize_tcp_m_devices(hash)
|
158
|
+
write(to_int(0, 'devices count', hash[:devices_count]), esize: 1)
|
159
|
+
hash[:devices].each do |device|
|
160
|
+
name = device[:name]
|
161
|
+
if device.key?(:name_length)
|
162
|
+
name_length = to_int(0, 'name length', device[:name_length])
|
163
|
+
unless name_length == name.length
|
164
|
+
raise InvalidMessageBody
|
165
|
+
.new(@msg_type, 'device name length and length of name' \
|
166
|
+
" mismatch: #{name_length} != #{name.length}")
|
167
|
+
end
|
168
|
+
else
|
169
|
+
name_length = name.length
|
170
|
+
end
|
171
|
+
|
172
|
+
rf_address, room_id =
|
173
|
+
to_ints(0, 'device RF address, room ID',
|
174
|
+
device[:rf_address], device[:room_id])
|
175
|
+
write(serialize(device_type_id(device[:type]), esize: 1) <<
|
176
|
+
serialize(rf_address, esize: 3) <<
|
177
|
+
serialize(device[:serial_number],
|
178
|
+
name_length, name, room_id, esize: 1))
|
179
|
+
end
|
180
|
+
end
|
181
|
+
end
|
182
|
+
end
|
183
|
+
end
|
184
|
+
end
|
185
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
|
2
|
+
module MaxCube
|
3
|
+
module Messages
|
4
|
+
module TCP
|
5
|
+
class Parser
|
6
|
+
module MessageN
|
7
|
+
private
|
8
|
+
|
9
|
+
KEYS = %i[device_type rf_address serial_number unknown].freeze
|
10
|
+
|
11
|
+
# New device (pairing) message
|
12
|
+
def parse_tcp_n(body)
|
13
|
+
@io = StringIO.new(decode(body), 'rb')
|
14
|
+
|
15
|
+
{
|
16
|
+
device_type: device_type(read(1, true)),
|
17
|
+
rf_address: read(3, true),
|
18
|
+
serial_number: read(10),
|
19
|
+
unknown: read(1),
|
20
|
+
}
|
21
|
+
rescue IOError
|
22
|
+
raise InvalidMessageBody
|
23
|
+
.new(@msg_type, 'unexpected EOF reached')
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
class Serializer
|
29
|
+
module MessageN
|
30
|
+
private
|
31
|
+
|
32
|
+
OPT_KEYS = %i[timeout].freeze
|
33
|
+
|
34
|
+
# Command to set the Cube into pairing mode
|
35
|
+
# with optional +timeout+ in seconds
|
36
|
+
def serialize_tcp_n(hash)
|
37
|
+
return '' unless hash.key?(:timeout)
|
38
|
+
format('%04x', to_int(0, 'timeout', hash[:timeout]))
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|