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.
Files changed (57) hide show
  1. checksums.yaml +7 -0
  2. data/.rubocop.yml +32 -0
  3. data/Gemfile +5 -0
  4. data/LICENSE.md +21 -0
  5. data/README.md +35 -0
  6. data/Rakefile +6 -0
  7. data/bin/console +8 -0
  8. data/bin/maxcube-client +31 -0
  9. data/bin/sample_server +13 -0
  10. data/bin/sample_socket +13 -0
  11. data/bin/setup +6 -0
  12. data/data/load/del +6 -0
  13. data/data/load/meta +20 -0
  14. data/data/load/ntp +6 -0
  15. data/data/load/set_temp +13 -0
  16. data/data/load/set_temp_mode +12 -0
  17. data/data/load/set_valve +11 -0
  18. data/data/load/url +4 -0
  19. data/data/load/wake +4 -0
  20. data/lib/maxcube/messages.rb +148 -0
  21. data/lib/maxcube/messages/handler.rb +154 -0
  22. data/lib/maxcube/messages/parser.rb +34 -0
  23. data/lib/maxcube/messages/serializer.rb +59 -0
  24. data/lib/maxcube/messages/tcp.rb +18 -0
  25. data/lib/maxcube/messages/tcp/handler.rb +70 -0
  26. data/lib/maxcube/messages/tcp/parser.rb +46 -0
  27. data/lib/maxcube/messages/tcp/serializer.rb +47 -0
  28. data/lib/maxcube/messages/tcp/type/a.rb +32 -0
  29. data/lib/maxcube/messages/tcp/type/c.rb +248 -0
  30. data/lib/maxcube/messages/tcp/type/f.rb +33 -0
  31. data/lib/maxcube/messages/tcp/type/h.rb +70 -0
  32. data/lib/maxcube/messages/tcp/type/l.rb +131 -0
  33. data/lib/maxcube/messages/tcp/type/m.rb +185 -0
  34. data/lib/maxcube/messages/tcp/type/n.rb +44 -0
  35. data/lib/maxcube/messages/tcp/type/q.rb +18 -0
  36. data/lib/maxcube/messages/tcp/type/s.rb +246 -0
  37. data/lib/maxcube/messages/tcp/type/t.rb +38 -0
  38. data/lib/maxcube/messages/tcp/type/u.rb +19 -0
  39. data/lib/maxcube/messages/tcp/type/z.rb +36 -0
  40. data/lib/maxcube/messages/udp.rb +9 -0
  41. data/lib/maxcube/messages/udp/handler.rb +40 -0
  42. data/lib/maxcube/messages/udp/parser.rb +50 -0
  43. data/lib/maxcube/messages/udp/serializer.rb +30 -0
  44. data/lib/maxcube/messages/udp/type/h.rb +24 -0
  45. data/lib/maxcube/messages/udp/type/i.rb +23 -0
  46. data/lib/maxcube/messages/udp/type/n.rb +21 -0
  47. data/lib/maxcube/network.rb +14 -0
  48. data/lib/maxcube/network/tcp.rb +11 -0
  49. data/lib/maxcube/network/tcp/client.rb +174 -0
  50. data/lib/maxcube/network/tcp/client/commands.rb +286 -0
  51. data/lib/maxcube/network/tcp/sample_server.rb +96 -0
  52. data/lib/maxcube/network/udp.rb +11 -0
  53. data/lib/maxcube/network/udp/client.rb +52 -0
  54. data/lib/maxcube/network/udp/sample_socket.rb +65 -0
  55. data/lib/maxcube/version.rb +4 -0
  56. data/maxcube-client.gemspec +29 -0
  57. 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,9 @@
1
+ require 'maxcube/messages'
2
+
3
+ module MaxCube
4
+ module Messages
5
+ module UDP
6
+ MSG_PREFIX = 'eQ3Max'.freeze
7
+ end
8
+ end
9
+ 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