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,18 @@
|
|
1
|
+
|
2
|
+
module MaxCube
|
3
|
+
module Messages
|
4
|
+
module TCP
|
5
|
+
class Serializer
|
6
|
+
module MessageQ
|
7
|
+
private
|
8
|
+
|
9
|
+
# Quit message - terminates connection
|
10
|
+
# Does not contain any data
|
11
|
+
def serialize_tcp_q(_hash)
|
12
|
+
''
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,246 @@
|
|
1
|
+
|
2
|
+
module MaxCube
|
3
|
+
module Messages
|
4
|
+
module TCP
|
5
|
+
class Parser
|
6
|
+
module MessageS
|
7
|
+
private
|
8
|
+
|
9
|
+
LENGTHS = [2, 1, 2].freeze
|
10
|
+
|
11
|
+
KEYS = %i[
|
12
|
+
duty_cycle
|
13
|
+
command_processed
|
14
|
+
free_memory_slots
|
15
|
+
].freeze
|
16
|
+
|
17
|
+
# Send command message (response)
|
18
|
+
def parse_tcp_s(body)
|
19
|
+
values = body.split(',')
|
20
|
+
check_msg_part_lengths(LENGTHS, *values)
|
21
|
+
values = to_ints(16, 'duty cycle, command result,' \
|
22
|
+
' free memory slots', *values)
|
23
|
+
values[1] = values[1].zero?
|
24
|
+
KEYS.zip(values).to_h
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
class Serializer
|
30
|
+
module MessageS
|
31
|
+
private
|
32
|
+
|
33
|
+
KEYS = %i[command].freeze
|
34
|
+
OPT_KEYS = %i[
|
35
|
+
unknown
|
36
|
+
rf_flags
|
37
|
+
rf_address_from rf_address_to rf_address rf_address_range
|
38
|
+
|
39
|
+
room_id mode temperature datetime_until
|
40
|
+
room_id day telegram_set program
|
41
|
+
|
42
|
+
room_id comfort_temperature eco_temperature
|
43
|
+
max_setpoint_temperature min_setpoint_temperature
|
44
|
+
temperature_offset window_open_temperature window_open_duration
|
45
|
+
|
46
|
+
room_id valve_opening boost_duration
|
47
|
+
decalcification_day decalcification_hour
|
48
|
+
max_valve_setting valve_offset
|
49
|
+
|
50
|
+
room_id partner_rf_address partner_type
|
51
|
+
room_id
|
52
|
+
room_id display_temperature
|
53
|
+
].freeze
|
54
|
+
|
55
|
+
COMMANDS = {
|
56
|
+
set_temperature_mode: 0x40,
|
57
|
+
set_program: 0x10,
|
58
|
+
set_temperature: 0x11,
|
59
|
+
config_valve: 0x12,
|
60
|
+
add_link_partner: 0x20,
|
61
|
+
remove_link_partner: 0x21,
|
62
|
+
set_group_address: 0x22,
|
63
|
+
unset_group_address: 0x23,
|
64
|
+
display_temperature: 0x82,
|
65
|
+
}.freeze
|
66
|
+
|
67
|
+
DEFAULT_RF_FLAGS = {
|
68
|
+
set_temperature_mode: 0x4,
|
69
|
+
set_program: 0x4,
|
70
|
+
set_temperature: 0x0,
|
71
|
+
config_valve: 0x4,
|
72
|
+
add_link_partner: 0x0,
|
73
|
+
remove_link_partner: 0x0,
|
74
|
+
set_group_address: 0x0,
|
75
|
+
unset_group_address: 0x0,
|
76
|
+
display_temperature: 0x0,
|
77
|
+
}.freeze
|
78
|
+
|
79
|
+
# Message to send command to Cube
|
80
|
+
def serialize_tcp_s(hash)
|
81
|
+
@io = StringIO.new('', 'wb')
|
82
|
+
|
83
|
+
cmd = serialize_tcp_s_head(hash)
|
84
|
+
send('serialize_tcp_s_' << cmd.to_s, hash)
|
85
|
+
|
86
|
+
encode(@io.string)
|
87
|
+
end
|
88
|
+
|
89
|
+
########################
|
90
|
+
|
91
|
+
def serialize_tcp_s_head_rf_address(hash)
|
92
|
+
rf_address_from = if hash.key?(:rf_address_from)
|
93
|
+
hash[:rf_address_from]
|
94
|
+
elsif hash.key?(:rf_address_range)
|
95
|
+
hash[:rf_address_range].min
|
96
|
+
else
|
97
|
+
0
|
98
|
+
end
|
99
|
+
rf_address_to = if hash.key?(:rf_address_to)
|
100
|
+
hash[:rf_address_to]
|
101
|
+
elsif hash.key?(:rf_address_range)
|
102
|
+
hash[:rf_address_range].max
|
103
|
+
else
|
104
|
+
hash[:rf_address]
|
105
|
+
end
|
106
|
+
to_ints(0, 'RF address range', rf_address_from, rf_address_to)
|
107
|
+
end
|
108
|
+
|
109
|
+
def serialize_tcp_s_head(hash)
|
110
|
+
command = hash[:command].to_sym
|
111
|
+
command_id = COMMANDS[command]
|
112
|
+
unless command_id
|
113
|
+
raise InvalidMessageBody
|
114
|
+
.new(@msg_type, "unknown command symbol: #{command}")
|
115
|
+
end
|
116
|
+
|
117
|
+
rf_flags = if hash.key?(:rf_flags)
|
118
|
+
to_int(0, 'RF flags', hash[:rf_flags])
|
119
|
+
else
|
120
|
+
DEFAULT_RF_FLAGS[command]
|
121
|
+
end
|
122
|
+
|
123
|
+
rf_address_from, rf_address_to =
|
124
|
+
serialize_tcp_s_head_rf_address(hash)
|
125
|
+
|
126
|
+
unknown = hash.key?(:unknown) ? hash[:unknown] : "\x00"
|
127
|
+
write(serialize(unknown, rf_flags, command_id, esize: 1) <<
|
128
|
+
serialize(rf_address_from, rf_address_to, esize: 3))
|
129
|
+
|
130
|
+
command
|
131
|
+
end
|
132
|
+
|
133
|
+
def serialize_tcp_s_set_temperature_mode(hash)
|
134
|
+
@mode = hash[:mode].to_sym
|
135
|
+
temp_mode = (to_float('temperature', hash[:temperature]) * 2).to_i |
|
136
|
+
device_mode_id(@mode) << 6
|
137
|
+
write(to_int(0, 'room ID', hash[:room_id]), temp_mode, esize: 1)
|
138
|
+
|
139
|
+
return unless @mode == :vacation
|
140
|
+
|
141
|
+
datetime_until = to_datetime('datetime until',
|
142
|
+
hash[:datetime_until])
|
143
|
+
|
144
|
+
year = datetime_until.year - 2000
|
145
|
+
month = datetime_until.month
|
146
|
+
day = datetime_until.day
|
147
|
+
date_until = year | (month & 1) << 7 |
|
148
|
+
day << 8 | (month & 0xe) << 12
|
149
|
+
|
150
|
+
hours = datetime_until.hour << 1
|
151
|
+
minutes = datetime_until.min < 30 ? 0 : 1
|
152
|
+
time_until = hours | minutes
|
153
|
+
|
154
|
+
write(serialize(date_until, esize: 2) <<
|
155
|
+
serialize(time_until, esize: 1))
|
156
|
+
end
|
157
|
+
|
158
|
+
def serialize_tcp_s_set_program(hash)
|
159
|
+
day_of_week = day_of_week_id(hash[:day])
|
160
|
+
day_of_week |= 0x8 if hash[:telegram_set]
|
161
|
+
write(to_int(0, 'room ID', hash[:room_id]), day_of_week, esize: 1)
|
162
|
+
|
163
|
+
hash[:program].each do |prog|
|
164
|
+
temp_time =
|
165
|
+
(to_float('temperature', prog[:temperature]) * 2).to_i << 9 |
|
166
|
+
(to_int(0, 'hours until', prog[:hours_until]) * 60 +
|
167
|
+
to_int(0, 'minutes until', prog[:minutes_until])) / 5
|
168
|
+
write(temp_time, esize: 2)
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
def serialize_tcp_s_set_temperature(hash)
|
173
|
+
keys = %i[comfort_temperature eco_temperature
|
174
|
+
max_setpoint_temperature min_setpoint_temperature
|
175
|
+
temperature_offset window_open_temperature].freeze
|
176
|
+
temperatures = hash.select { |k| keys.include?(k) }
|
177
|
+
.map { |k, v| to_float(k, v) * 2 }
|
178
|
+
temperatures[-2] += 7
|
179
|
+
|
180
|
+
open_duration = to_int(0, 'window open duration',
|
181
|
+
hash[:window_open_duration]) / 5
|
182
|
+
write(to_int(0, 'room ID', hash[:room_id]),
|
183
|
+
*temperatures.map(&:to_i), open_duration, esize: 1)
|
184
|
+
end
|
185
|
+
|
186
|
+
def serialize_tcp_s_config_valve(hash)
|
187
|
+
boost_duration =
|
188
|
+
[7, to_int(0, 'boost duration', hash[:boost_duration]) / 5].min
|
189
|
+
valve_opening = to_float('valve opening', hash[:valve_opening])
|
190
|
+
.round / 5
|
191
|
+
boost = boost_duration << 5 | valve_opening
|
192
|
+
|
193
|
+
decalcification_day = day_of_week_id(hash[:decalcification_day])
|
194
|
+
decalcification = decalcification_day << 5 |
|
195
|
+
to_int(0, 'decalcification hour',
|
196
|
+
hash[:decalcification_hour])
|
197
|
+
|
198
|
+
percent = %i[max_valve_setting valve_offset]
|
199
|
+
.map { |k| (to_float(k, hash[k]) * 2.55).round }
|
200
|
+
|
201
|
+
write(to_int(0, 'room ID', hash[:room_id]),
|
202
|
+
boost, decalcification, *percent, esize: 1)
|
203
|
+
end
|
204
|
+
|
205
|
+
def serialize_tcp_s_link_partner(hash)
|
206
|
+
partner_type = device_type_id(hash[:partner_type])
|
207
|
+
write(serialize(to_int(0, 'room ID', hash[:room_id]), esize: 1) <<
|
208
|
+
serialize(to_int(0, 'partner RF address',
|
209
|
+
hash[:partner_rf_address]), esize: 3) <<
|
210
|
+
serialize(partner_type, esize: 1))
|
211
|
+
end
|
212
|
+
|
213
|
+
def serialize_tcp_s_add_link_partner(hash)
|
214
|
+
serialize_tcp_s_link_partner(hash)
|
215
|
+
end
|
216
|
+
|
217
|
+
def serialize_tcp_s_remove_link_partner(hash)
|
218
|
+
serialize_tcp_s_link_partner(hash)
|
219
|
+
end
|
220
|
+
|
221
|
+
def serialize_tcp_s_group_address(hash)
|
222
|
+
write(0, to_int(0, 'room ID', hash[:room_id]), esize: 1)
|
223
|
+
end
|
224
|
+
|
225
|
+
def serialize_tcp_s_set_group_address(hash)
|
226
|
+
serialize_tcp_s_group_address(hash)
|
227
|
+
end
|
228
|
+
|
229
|
+
def serialize_tcp_s_unset_group_address(hash)
|
230
|
+
serialize_tcp_s_group_address(hash)
|
231
|
+
end
|
232
|
+
|
233
|
+
# Works only on wall thermostats
|
234
|
+
def serialize_tcp_s_display_temperature(hash)
|
235
|
+
display_settings = Hash.new(0)
|
236
|
+
.merge!(measured: 4, configured: 0)
|
237
|
+
.freeze
|
238
|
+
display = display_settings[hash.fetch(:display_temperature, 'x')
|
239
|
+
.to_sym]
|
240
|
+
write(to_int(0, 'room ID', hash[:room_id]), display, esize: 1)
|
241
|
+
end
|
242
|
+
end
|
243
|
+
end
|
244
|
+
end
|
245
|
+
end
|
246
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
|
2
|
+
module MaxCube
|
3
|
+
module Messages
|
4
|
+
module TCP
|
5
|
+
class Serializer
|
6
|
+
module MessageT
|
7
|
+
private
|
8
|
+
|
9
|
+
# +count+ argument would cause ambuigity if it was optional
|
10
|
+
# due to +rf_addresses+ has variable size
|
11
|
+
KEYS = %i[count force rf_addresses].freeze
|
12
|
+
|
13
|
+
# Command to delete one or more devices from the Cube
|
14
|
+
# Acknowledgement (A) follows
|
15
|
+
def serialize_tcp_t(hash)
|
16
|
+
force = to_bool('force mode', hash[:force]) ? '1' : '0'
|
17
|
+
rf_addresses = to_ints(0, 'RF addresses', *hash[:rf_addresses])
|
18
|
+
count = to_int(0, 'count', hash[:count])
|
19
|
+
|
20
|
+
unless count == rf_addresses.size
|
21
|
+
raise InvalidMessageBody
|
22
|
+
.new(@msg_type,
|
23
|
+
'count and number of devices mismatch: ' \
|
24
|
+
"#{count} != #{rf_addresses.size}")
|
25
|
+
end
|
26
|
+
if count.zero?
|
27
|
+
raise InvalidMessageBody
|
28
|
+
.new(@msg_type, 'no device specified')
|
29
|
+
end
|
30
|
+
|
31
|
+
addrs = encode(serialize(*rf_addresses, esize: 3))
|
32
|
+
[format('%02x', count), force, addrs].join(',')
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
|
2
|
+
module MaxCube
|
3
|
+
module Messages
|
4
|
+
module TCP
|
5
|
+
class Serializer
|
6
|
+
module MessageU
|
7
|
+
private
|
8
|
+
|
9
|
+
KEYS = %i[url port].freeze
|
10
|
+
|
11
|
+
# Command to configure Cube's portal URL
|
12
|
+
def serialize_tcp_u(hash)
|
13
|
+
"#{hash[:url]}:#{to_int(0, 'port', hash[:port])}"
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
|
2
|
+
module MaxCube
|
3
|
+
module Messages
|
4
|
+
module TCP
|
5
|
+
class Serializer
|
6
|
+
module MessageZ
|
7
|
+
private
|
8
|
+
|
9
|
+
KEYS = %i[time scope].freeze
|
10
|
+
OPT_KEYS = %i[id].freeze
|
11
|
+
|
12
|
+
# Wakeup command
|
13
|
+
# Acknowledgement (A) follows
|
14
|
+
def serialize_tcp_z(hash)
|
15
|
+
time = format('%02x', to_int(0, 'time', hash[:time]))
|
16
|
+
scope = hash[:scope].to_sym
|
17
|
+
scope = case scope
|
18
|
+
when :group, :room
|
19
|
+
'G'
|
20
|
+
when :all
|
21
|
+
'A'
|
22
|
+
when :device
|
23
|
+
'D'
|
24
|
+
else
|
25
|
+
raise InvalidMessageBody.new(@msg_type,
|
26
|
+
"invalid scope: #{scope}")
|
27
|
+
end
|
28
|
+
args = [time, scope]
|
29
|
+
args << format('%02x', to_int(0, 'ID', hash[:id])) if hash.key?(:id)
|
30
|
+
args.join(',')
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
require 'maxcube/messages/udp'
|
2
|
+
require 'maxcube/messages/handler'
|
3
|
+
|
4
|
+
module MaxCube
|
5
|
+
module Messages
|
6
|
+
module UDP
|
7
|
+
module Handler
|
8
|
+
include Messages::Handler
|
9
|
+
|
10
|
+
def valid_udp_msg_prefix(msg)
|
11
|
+
msg.start_with?(self.class.const_get('MSG_PREFIX'))
|
12
|
+
end
|
13
|
+
|
14
|
+
def check_udp_msg_prefix(msg)
|
15
|
+
raise InvalidMessageFormat unless valid_udp_msg_prefix(msg)
|
16
|
+
end
|
17
|
+
|
18
|
+
def valid_udp_msg(msg)
|
19
|
+
valid_udp_msg_prefix(msg) &&
|
20
|
+
valid_msg(msg)
|
21
|
+
end
|
22
|
+
|
23
|
+
def check_udp_msg(msg)
|
24
|
+
check_udp_msg_prefix(msg)
|
25
|
+
check_msg(msg)
|
26
|
+
msg
|
27
|
+
end
|
28
|
+
|
29
|
+
def valid_udp_hash(hash)
|
30
|
+
valid_hash(hash)
|
31
|
+
end
|
32
|
+
|
33
|
+
def check_udp_hash(hash)
|
34
|
+
check_hash(hash)
|
35
|
+
hash
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,50 @@
|
|
1
|
+
require_relative 'handler'
|
2
|
+
require 'maxcube/messages/parser'
|
3
|
+
|
4
|
+
module MaxCube
|
5
|
+
module Messages
|
6
|
+
module UDP
|
7
|
+
class Parser
|
8
|
+
include Handler
|
9
|
+
include Messages::Parser
|
10
|
+
|
11
|
+
KEYS = %i[prefix serial_number id].freeze
|
12
|
+
|
13
|
+
%w[i n h].each { |f| require_relative 'type/' << f }
|
14
|
+
|
15
|
+
MSG_TYPES = %w[I N h c].freeze
|
16
|
+
|
17
|
+
include MessageI
|
18
|
+
include MessageN
|
19
|
+
include MessageH
|
20
|
+
|
21
|
+
MSG_PREFIX = (UDP::MSG_PREFIX + 'Ap').freeze
|
22
|
+
|
23
|
+
def parse_udp_msg(msg)
|
24
|
+
check_udp_msg(msg)
|
25
|
+
hash = parse_udp_msg_head(msg)
|
26
|
+
return hash unless parse_msg_body(@io.string, hash, 'udp')
|
27
|
+
check_udp_hash(hash)
|
28
|
+
end
|
29
|
+
|
30
|
+
private
|
31
|
+
|
32
|
+
def msg_msg_type(msg)
|
33
|
+
msg[19]
|
34
|
+
end
|
35
|
+
|
36
|
+
def parse_udp_msg_head(msg)
|
37
|
+
@io = StringIO.new(msg, 'rb')
|
38
|
+
hash = {
|
39
|
+
prefix: read(8),
|
40
|
+
serial_number: read(10),
|
41
|
+
id: read(1, true),
|
42
|
+
type: read(1),
|
43
|
+
}
|
44
|
+
@io.string = @io.read
|
45
|
+
hash
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|