maxcube-client 0.5.0 → 0.5.1

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 (95) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +4 -0
  3. data/data/save/20180214-2023/recv.data +12 -0
  4. data/data/save/20180214-2023/recv.yaml +152 -0
  5. data/data/save/20180214-2023/sent.data +10 -0
  6. data/data/save/20180214-2023/sent.yaml +26 -0
  7. data/data/save/20180214-2230/recv.data +2 -0
  8. data/data/save/20180214-2230/recv.yaml +68 -0
  9. data/data/save/20180214-2230/sent.data +1 -0
  10. data/data/save/20180214-2230/sent.yaml +19 -0
  11. data/data/save/20180215-0057/recv.data +3 -0
  12. data/data/save/20180215-0057/recv.yaml +69 -0
  13. data/data/save/20180215-0057/sent.data +2 -0
  14. data/data/save/20180215-0057/sent.yaml +22 -0
  15. data/data/save/20180216-1641/recv.data +4 -0
  16. data/data/save/20180216-1641/recv.yaml +133 -0
  17. data/data/save/20180216-1641/sent.data +2 -0
  18. data/data/save/20180216-1641/sent.yaml +3 -0
  19. data/lib/maxcube/version.rb +1 -1
  20. data/maxcube-client.gemspec +5 -6
  21. data/spec/msg_spec.rb +2117 -0
  22. data/spec/spec_helper.rb +7 -0
  23. metadata +21 -75
  24. data/.gitignore +0 -6
  25. data/.rubocop.yml +0 -29
  26. data/.yardopts +0 -1
  27. data/Gemfile +0 -5
  28. data/bin/console +0 -8
  29. data/bin/maxcube-client +0 -6
  30. data/bin/sample_server +0 -13
  31. data/bin/sample_socket +0 -13
  32. data/bin/setup +0 -6
  33. data/doc/MaxCube.html +0 -502
  34. data/doc/MaxCube/Messages.html +0 -1927
  35. data/doc/MaxCube/Messages/Handler.html +0 -2912
  36. data/doc/MaxCube/Messages/InvalidMessage.html +0 -140
  37. data/doc/MaxCube/Messages/InvalidMessageBody.html +0 -264
  38. data/doc/MaxCube/Messages/InvalidMessageFormat.html +0 -247
  39. data/doc/MaxCube/Messages/InvalidMessageLength.html +0 -247
  40. data/doc/MaxCube/Messages/InvalidMessageType.html +0 -263
  41. data/doc/MaxCube/Messages/Parser.html +0 -520
  42. data/doc/MaxCube/Messages/Serializer.html +0 -701
  43. data/doc/MaxCube/Messages/TCP.html +0 -172
  44. data/doc/MaxCube/Messages/TCP/Handler.html +0 -1396
  45. data/doc/MaxCube/Messages/TCP/Parser.html +0 -462
  46. data/doc/MaxCube/Messages/TCP/Parser/MessageA.html +0 -186
  47. data/doc/MaxCube/Messages/TCP/Parser/MessageC.html +0 -1077
  48. data/doc/MaxCube/Messages/TCP/Parser/MessageF.html +0 -206
  49. data/doc/MaxCube/Messages/TCP/Parser/MessageH.html +0 -338
  50. data/doc/MaxCube/Messages/TCP/Parser/MessageL.html +0 -535
  51. data/doc/MaxCube/Messages/TCP/Parser/MessageM.html +0 -510
  52. data/doc/MaxCube/Messages/TCP/Parser/MessageN.html +0 -226
  53. data/doc/MaxCube/Messages/TCP/Parser/MessageS.html +0 -225
  54. data/doc/MaxCube/Messages/TCP/Serializer.html +0 -460
  55. data/doc/MaxCube/Messages/TCP/Serializer/MessageA.html +0 -186
  56. data/doc/MaxCube/Messages/TCP/Serializer/MessageC.html +0 -185
  57. data/doc/MaxCube/Messages/TCP/Serializer/MessageF.html +0 -206
  58. data/doc/MaxCube/Messages/TCP/Serializer/MessageL.html +0 -185
  59. data/doc/MaxCube/Messages/TCP/Serializer/MessageM.html +0 -428
  60. data/doc/MaxCube/Messages/TCP/Serializer/MessageN.html +0 -209
  61. data/doc/MaxCube/Messages/TCP/Serializer/MessageQ.html +0 -185
  62. data/doc/MaxCube/Messages/TCP/Serializer/MessageS.html +0 -1168
  63. data/doc/MaxCube/Messages/TCP/Serializer/MessageT.html +0 -240
  64. data/doc/MaxCube/Messages/TCP/Serializer/MessageU.html +0 -206
  65. data/doc/MaxCube/Messages/TCP/Serializer/MessageZ.html +0 -252
  66. data/doc/MaxCube/Messages/UDP.html +0 -164
  67. data/doc/MaxCube/Messages/UDP/Handler.html +0 -832
  68. data/doc/MaxCube/Messages/UDP/Parser.html +0 -609
  69. data/doc/MaxCube/Messages/UDP/Parser/MessageH.html +0 -218
  70. data/doc/MaxCube/Messages/UDP/Parser/MessageI.html +0 -215
  71. data/doc/MaxCube/Messages/UDP/Parser/MessageN.html +0 -226
  72. data/doc/MaxCube/Messages/UDP/Serializer.html +0 -484
  73. data/doc/MaxCube/Network.html +0 -167
  74. data/doc/MaxCube/Network/TCP.html +0 -150
  75. data/doc/MaxCube/Network/TCP/Client.html +0 -1930
  76. data/doc/MaxCube/Network/TCP/Client/Commands.html +0 -2457
  77. data/doc/MaxCube/Network/TCP/SampleServer.html +0 -910
  78. data/doc/MaxCube/Network/UDP.html +0 -150
  79. data/doc/MaxCube/Network/UDP/Client.html +0 -518
  80. data/doc/MaxCube/Network/UDP/SampleSocket.html +0 -628
  81. data/doc/MaxCube/Runner.html +0 -355
  82. data/doc/_index.html +0 -518
  83. data/doc/class_list.html +0 -51
  84. data/doc/css/common.css +0 -1
  85. data/doc/css/full_list.css +0 -58
  86. data/doc/css/style.css +0 -499
  87. data/doc/file.README.html +0 -140
  88. data/doc/file_list.html +0 -56
  89. data/doc/frames.html +0 -17
  90. data/doc/index.html +0 -140
  91. data/doc/js/app.js +0 -248
  92. data/doc/js/full_list.js +0 -216
  93. data/doc/js/jquery.js +0 -4
  94. data/doc/method_list.html +0 -1699
  95. data/doc/top-level-namespace.html +0 -110
@@ -0,0 +1,2 @@
1
+ c:
2
+ l:
@@ -0,0 +1,3 @@
1
+ ---
2
+ - - :type: c
3
+ - - :type: l
@@ -1,5 +1,5 @@
1
1
 
2
2
  module MaxCube
3
3
  # Project (package) version
4
- VERSION = '0.5.0'.freeze
4
+ VERSION = '0.5.1'.freeze
5
5
  end
@@ -13,15 +13,14 @@ Gem::Specification.new do |spec|
13
13
  spec.homepage = "https://github.com/Tomaqa/maxcube-client"
14
14
  spec.license = "MIT"
15
15
 
16
- spec.files = `git ls-files -z`.split("\x0").reject do |f|
17
- f.match(%r{^(test|spec|features)/})
18
- end
19
- spec.bindir = "exe"
20
- spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
16
+ spec.files = Dir['data/**/*', 'lib/**/*', 'spec/**/*',
17
+ '*.gemspec', 'LICENSE*', 'README*', 'Rakefile']
18
+ spec.bindir = "bin"
19
+ spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
21
20
  spec.require_paths = ["lib"]
22
21
  spec.metadata['yard.run'] = 'yard'
23
22
 
24
- spec.required_ruby_version = '>= 2.2'
23
+ spec.required_ruby_version = '>= 2.3'
25
24
 
26
25
  spec.add_development_dependency "bundler", "~> 1.16"
27
26
  spec.add_development_dependency "rake", "~> 10.0"
@@ -0,0 +1,2117 @@
1
+ require 'maxcube/messages/tcp/parser'
2
+ require 'maxcube/messages/tcp/serializer'
3
+ require_relative 'spec_helper'
4
+
5
+ include MaxCube
6
+
7
+ describe 'TCP::Parser' do
8
+ subject(:parser) { Messages::TCP::Parser.new }
9
+
10
+ # Proper message examples:
11
+ # A:\r\n
12
+ # H:KEQ0523864,097f2c,0113,00000000,477719c0,00,32,0d0c09,1404,03,0000\r\n
13
+ # L:Cw/a7QkSGBgoAMwACw/DcwkSGBgoAM8ACw/DgAkSGBgoAM4A\r\n
14
+
15
+ describe 'invalid data' do
16
+ context 'empty message' do
17
+ let(:inputs) do
18
+ [
19
+ '',
20
+ ]
21
+ end
22
+ it 'returns empty array' do
23
+ inputs.each do |i|
24
+ expect(parser.parse_tcp_data(i)).to eq([])
25
+ end
26
+ end
27
+ end
28
+
29
+ # context 'invalid type' do
30
+ # let(:inputs) do
31
+ # [
32
+ # nil,
33
+ # 0,
34
+ # 1,
35
+ # 1.5,
36
+ # /abc/,
37
+ # Object.new,
38
+ # [],
39
+ # {},
40
+ # ]
41
+ # end
42
+ # it 'raises proper exception' do
43
+ # inputs.each do |inp|
44
+ # expect { parser.parse_tcp_data(inp) }.to raise_error TypeError
45
+ # end
46
+ # end
47
+ # end
48
+
49
+ context 'invalid format' do
50
+ context 'of single message' do
51
+ let(:msgs) do
52
+ [
53
+ '::',
54
+ 'HX:',
55
+ 'HX:A',
56
+ 'HX:A:',
57
+ '1:',
58
+ "A:\x00",
59
+ "A:\r",
60
+ "A:\n",
61
+ "A:\n\r",
62
+ ]
63
+ end
64
+ it 'raises proper exception and #valid_tcp_msg_format and #valid_tcp_msg are falsey' do
65
+ msgs.each do |m|
66
+ expect { parser.parse_tcp_msg(m) }.to raise_error Messages::InvalidMessageFormat
67
+ expect(parser.valid_tcp_msg_format(m)).to be_falsey
68
+ expect(parser.valid_tcp_msg(m)).to be_falsey
69
+ end
70
+ end
71
+ it 'raises proper exception when passed as raw data' do
72
+ msgs.each do |m|
73
+ expect { parser.parse_tcp_data(m) }.to raise_error Messages::InvalidMessageFormat
74
+ end
75
+ end
76
+ end
77
+
78
+ context 'of raw data' do
79
+ let(:data) do
80
+ [
81
+ "\r\n",
82
+ "\r\n\r\n",
83
+ 'A:\r\n',
84
+ 'A:\n',
85
+ 'A:\r',
86
+ 'A:\r\nA:\r\n',
87
+ "A:\r\nA:\r\naX:\r\n",
88
+ "H:KEQ0523864,097f2c,0113,00000000,477719c0,00,32,0d0c09,1404,03,0000\r\nA:\r\nA:\r\naX:\r\n",
89
+ "A:\r\nH:KEQ0523864,097f2c,0113,00000000,477719c0,00,32,0d0c09,1404,03,0000\r\nA:\r\naX:\r\n",
90
+ "A:\nH:KEQ0523864,097f2c,0113,00000000,477719c0,00,32,0d0c09,1404,03,0000\r\nA:\r\naX:\r\n",
91
+ ]
92
+ end
93
+ it 'raises proper exception' do
94
+ data.each do |d|
95
+ expect { parser.parse_tcp_data(d) }.to raise_error Messages::InvalidMessageFormat
96
+ end
97
+ end
98
+ end
99
+ end
100
+
101
+ context 'invalid message type' do
102
+ let(:msgs) do
103
+ [
104
+ 'X:',
105
+ 'h:',
106
+ 'u:',
107
+ 'x:',
108
+ 'l:',
109
+ ]
110
+ end
111
+ it 'raises proper exception and #valid_msg_msg_type and #valid_tcp_msg are falsey' do
112
+ msgs.each do |m|
113
+ expect { parser.parse_tcp_msg(m) }.to raise_error Messages::InvalidMessageType
114
+ expect(parser.valid_msg_msg_type(m)).to be_falsey
115
+ expect(parser.valid_tcp_msg(m)).to be_falsey
116
+ end
117
+ end
118
+ end
119
+
120
+ context 'valid message type but parser not implemented (yet)' do
121
+ let(:msgs) do
122
+ [
123
+ 'D:ZXhhbXBsZQoAAAAAAAAAAA==',
124
+ 'E:encrypted-base64',
125
+ 'w:message1,message2,0001',
126
+ ]
127
+ end
128
+ let(:ret) do
129
+ [
130
+ { type: 'D', data: 'ZXhhbXBsZQoAAAAAAAAAAA==' },
131
+ { type: 'E', data: 'encrypted-base64' },
132
+ { type: 'w', data: 'message1,message2,0001'},
133
+ ]
134
+ end
135
+ it 'returns hash with unparsed data' do
136
+ msgs.each_with_index do |m, i|
137
+ expect(parser.parse_tcp_msg(m)).to eq(ret[i])
138
+ end
139
+ end
140
+ end
141
+
142
+ context 'invalid message length' do
143
+ let(:msgs) do
144
+ [
145
+ 'A',
146
+ ':',
147
+ 'A:' + 'x' * Messages::TCP::MSG_MAX_LEN,
148
+ ]
149
+ end
150
+ it 'raises proper exception and #valid_tcp_msg_length and #valid_tcp_msg are falsey' do
151
+ msgs.each do |m|
152
+ expect { parser.parse_tcp_msg(m) }.to raise_error Messages::InvalidMessageLength
153
+ expect(parser.valid_tcp_msg_length(m)).to be_falsey
154
+ expect(parser.valid_tcp_msg(m)).to be_falsey
155
+ end
156
+ end
157
+ end
158
+ end
159
+
160
+ describe 'valid data' do
161
+ let(:data) do
162
+ [
163
+ "A:\r\nH:KEQ0523864,097f2c,0113,00000000,477719c0,00,32,0d0c09,1404,03,0000\r\n",
164
+ "A:abcde\r\nA:\r\n",
165
+ "H:KEQ0523864,097f2c,0113,00000000,477719c0,00,32,0d0c09,1404,03,0000\r\n"\
166
+ "H:KEQ0523864,097f2c,0113,00000000,477719c0,00,32,0d0c09,1404,03,0000\r\n",
167
+ "A:\r\nA:\r\n"\
168
+ "H:KEQ0523864,097f2c,0113,00000000,477719c0,00,32,0d0c09,1404,03,0000\r\n"\
169
+ "A:\r\n"\
170
+ "M:00,01,VgIEAQNCYWQK7WkCBEJ1cm8K8wADCldvaG56aW1tZXIK8wwEDFNjaGxhZnppbW1lcgr"\
171
+ "1QAUCCu1pS0VRMDM3ODA0MAZIVCBCYWQBAgrzAEtFUTAzNzk1NDQHSFQgQnVybwICCvMMS0VRMD"\
172
+ "M3OTU1NhlIVCBXb2huemltbWVyIEJhbGtvbnNlaXRlAwIK83lLRVEwMzc5NjY1GkhUIFdvaG56a"\
173
+ "W1tZXIgRmVuc3RlcnNlaXRlAwIK9UBLRVEwMzgwMTIwD0hUIFNjaGxhZnppbW1lcgQB\r\n"\
174
+ "A:\r\n",
175
+ ]
176
+ end
177
+ let(:res) do
178
+ [
179
+ %w[A H],
180
+ %w[A A],
181
+ %w[H H],
182
+ %w[A A H A M A],
183
+ ]
184
+ end
185
+ it 'returns array of hashes with proper message types' do
186
+ data.each_with_index do |d, i|
187
+ ary = parser.parse_tcp_data(d)
188
+ expect(ary).to be_an(Array).and all(be_a(Hash))
189
+ ary.each_with_index do |h, j|
190
+ expect(h).to include(:type)
191
+ expect(h[:type]).to eq(res[i][j])
192
+ end
193
+ end
194
+ end
195
+ end
196
+
197
+ describe 'concrete message types' do
198
+
199
+ describe 'A message' do
200
+ let(:msgs) do
201
+ [
202
+ 'A:',
203
+ 'A:acknowledgement!',
204
+ 'A:\r\n',
205
+ 'A:1234abcd',
206
+ ]
207
+ end
208
+ it 'ignores any content of message' do
209
+ msgs.each do |m|
210
+ expect(parser.parse_tcp_msg(m)).to eq({ type: 'A' })
211
+ end
212
+ end
213
+ end
214
+
215
+ describe 'C message' do
216
+ context 'invalid message body' do
217
+ let(:msgs) do
218
+ [
219
+ # invalid address
220
+ 'C:06c94,' + Base64.strict_encode64("\x12\x06\xc9\x41\x04\x01\x18\xff\x4b\x45\x51\x30\x33\x35\x32\x32\x37\x20"),
221
+ 'C:06c94x,' + Base64.strict_encode64("\x12\x06\xc9\x41\x04\x01\x18\xff\x4b\x45\x51\x30\x33\x35\x32\x32\x37\x20"),
222
+ # insufficient head length
223
+ 'C:06c941,' + Base64.strict_encode64("\x11\x06\xc9\x41\x00\x01\x18\xff\x4b\x45\x51\x30\x33\x35\x32\x32\x37"),
224
+ 'C:06c941,' + Base64.strict_encode64("\x12\x06\xc9\x41\x00\x01\x18\xff\x4b\x45\x51\x30\x33\x35\x32\x32\x37"),
225
+ 'C:06c941,' + Base64.strict_encode64("\x11\x06\xc9\x41\x01\x01\x18\xff\x4b\x45\x51\x30\x33\x35\x32\x32\x37"),
226
+ 'C:06c941,' + Base64.strict_encode64("\x12\x06\xc9\x41\x01\x01\x18\xff\x4b\x45\x51\x30\x33\x35\x32\x32\x37"),
227
+ 'C:06c941,' + Base64.strict_encode64("\x12\x06\xc9\x41\x02\x01\x18\xff\x4b\x45\x51\x30\x33\x35\x32\x32\x37"),
228
+ 'C:06c941,' + Base64.strict_encode64("\x11\x06\xc9\x41\x04\x01\x18\xff\x4b\x45\x51\x30\x33\x35\x32\x32\x37"),
229
+ 'C:06c941,' + Base64.strict_encode64("\x12\x06\xc9\x41\x04\x01\x18\xff\x4b\x45\x51\x30\x33\x35\x32\x32\x37"),
230
+ # insufficient body length
231
+ 'C:06c941,' + Base64.strict_encode64("\x13\x06\xc9\x41\x01\x01\x18\xff\x4b\x45\x51\x30\x33\x35\x32\x32\x37\x36"),
232
+ 'C:06c941,' + Base64.strict_encode64("\x13\x06\xc9\x41\x02\x01\x18\xff\x4b\x45\x51\x30\x33\x35\x32\x32\x37\x36"),
233
+ 'C:06c941,' + Base64.strict_encode64("\x13\x06\xc9\x41\x03\x01\x18\xff\x4b\x45\x51\x30\x33\x35\x32\x32\x37\x36"),
234
+ # invalid device type
235
+ 'C:06c941,' + Base64.strict_encode64("\x12\x06\xc9\x41\x06\x01\x18\xff\x4b\x45\x51\x30\x33\x35\x32\x32\x37\x36"),
236
+ ]
237
+ end
238
+ it 'raises proper exception' do
239
+ msgs.each do |m|
240
+ expect{ parser.parse_tcp_msg(m) }.to raise_error Messages::InvalidMessageBody
241
+ end
242
+ end
243
+ end
244
+
245
+ context 'valid message body' do
246
+ let(:msgs) do
247
+ [
248
+ # cube data
249
+ 'C:03f6c9,7QP2yQATAf9KRVEwNTQzNTQ1AQsABEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAsABEAAAAAAAAAAQQAAAAAAAAAAAAAAAAAAAAAAAAAAAGh0dHA6Ly93d3cubWF4LXBvcnRhbC5lbHYuZGU6ODAvY3ViZQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAENFVAAACgADAAAOEENFU1QAAwACAAAcIA==',
250
+ 'C:10b199,7RCxmQATAf9MRVExMTU0NzI3AAsABEAAAAAAAAAAAP///////////////////////////wsABEAAAAAAAAAAQf///////////////////////////2h0dHA6Ly9tYXguZXEtMy5kZTo4MC9jdWJlADAvbG9va3VwAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAENFVAAACgADAAAOEENFU1QAAwACAAAcIA==',
251
+ 'C:03f25d,' + Base64.strict_encode64("\xed\x03\xf2\x5d\x00\x13\x01\x00\x4a\x45\x51\x30\x35\x34\x34\x39\x32\x33\x00\x0b\x00\x04\x40\x00\x00\x00\x00\x00\x00\x00\x00\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x0b\x00\x04\x40\x00\x00\x00\x00\x00\x00\x00\x41\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\xff\x68\x74\x74\x70\x3a\x2f\x2f\x6d\x61\x78\x2e\x65\x71\x2d\x33\x2e\x64\x65\x3a\x38\x30\x2f\x63\x75\x62\x65\x00\x30\x2f\x6c\x6f\x6f\x6b\x75\x70\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x43\x45\x54\x00\x00\x0a\x00\x03\x00\x00\x0e\x10\x43\x45\x53\x54\x00\x03\x00\x02\x00\x00\x1c\x20"),
252
+ # radiator thermostat data
253
+ 'C:0b04be,0gsEvgIBEP9LRVEwNTcxNjc0LCE9CQcYA1CH/wBETlxmWPxVFEUgRSBFIEUgRSBFIEUgRSBFIEROXGZY/FUURSBFIEUgRSBFIEUgRSBFIEUgRE5cZlj8VRRFIEUgRSBFIEUgRSBFIEUgRSBETlxmWPxVFEUgRSBFIEUgRSBFIEUgRSBFIEROXGZY/FUURSBFIEUgRSBFIEUgRSBFIEUgRE5cZlj8VRRFIEUgRSBFIEUgRSBFIEUgRSBETlxmWPxVFEUgRSBFIEUgRSBFIEUgRSBFIA==',
254
+ 'C:08b6d2,0gi20gICEABLRVEwNjM0NjA3LiE9CQcYA1AM/wBETlxmWwhVFEUgRSBFIEUgRSBFIEUgRSBFIEROXGZbCFUURSBFIEUgRSBFIEUgRSBFIEUgRE5cZlsIVRRFIEUgRSBFIEUgRSBFIEUgRSBETlxmWwhVFEUgRSBFIEUgRSBFIEUgRSBFIEROXGZbCFUURSBFIEUgRSBFIEUgRSBFIEUgRE5cZlsIVRRFIEUgRSBFIEUgRSBFIEUgRSBETlxmWwhVFEUgRSBFIEUgRSBFIEUgRSBFIA==',
255
+ 'C:122b65,0hIrZQIBEABNRVExNDcyOTk3Oyc9CQcYA5IM/wBESHkPRSBFIEUgRSBFIEUgRSBFIEUgRSBFIERIeQlFIEUgRSBFIEUgRSBFIEUgRSBFIEUgREJ4XkTJeRJFIEUgRSBFIEUgRSBFIEUgRSBEQnheRMl5EkUgRSBFIEUgRSBFIEUgRSBFIERCeF5EyXkSRSBFIEUgRSBFIEUgRSBFIEUgREJ4XkTJeRJFIEUgRSBFIEUgRSBFIEUgRSBEQnheRMl5EkUgRSBFIEUgRSBFIEUgRSBFIA==',
256
+ 'C:06c941,' + Base64.strict_encode64("\xd2\x06\xc9\x41\x01\x01\x18\xff\x4b\x45\x51\x30\x33\x35\x32\x32\x37\x36\x24\x20\x3d\x09\x07\x18\x03\xf4\x0c\xff\x00\x41\x20\x41\x20\x41\x20\x41\x20\x41\x20\x41\x20\x41\x20\x45\x20\x45\x20\x45\x20\x45\x20\x45\x20\x45\x20\x41\x20\x41\x20\x41\x20\x41\x20\x41\x20\x41\x20\x41\x20\x45\x20\x45\x20\x45\x20\x45\x20\x45\x20\x45\x20\x41\x20\x41\x20\x41\x20\x41\x20\x41\x20\x41\x20\x41\x20\x45\x20\x45\x20\x45\x20\x45\x20\x45\x20\x45\x20\x41\x20\x41\x20\x41\x20\x41\x20\x41\x20\x41\x20\x41\x20\x45\x20\x45\x20\x45\x20\x45\x20\x45\x20\x45\x20\x41\x20\x41\x20\x41\x20\x41\x20\x41\x20\x41\x20\x41\x20\x45\x20\x45\x20\x45\x20\x45\x20\x45\x20\x45\x20\x41\x20\x41\x20\x41\x20\x41\x20\x41\x20\x41\x20\x41\x20\x45\x20\x45\x20\x45\x20\x45\x20\x45\x20\x45\x20\x41\x20\x41\x20\x41\x20\x41\x20\x41\x20\x41\x20\x41\x20\x45\x20\x45\x20\x45\x20\x45\x20\x45\x20\x45\x20\x06\x18\xf4"),
257
+ # wall thermostat data
258
+ 'C:121707,zhIXBwMBEP9NRVEwODUzNjAzKyE9CURIVQhFIEUgRSBFIEUgRSBFIEUgRSBFIEUgREhVCEUgRSBFIEUgRSBFIEUgRSBFIEUgRSBESFRsRMxVFEUgRSBFIEUgRSBFIEUgRSBFIERIVGxEzFUURSBFIEUgRSBFIEUgRSBFIEUgREhUbETMVRRFIEUgRSBFIEUgRSBFIEUgRSBESFRsRMxVFEUgRSBFIEUgRSBFIEUgRSBFIERIVGxEzFUURSBFIEUgRSBFIEUgRSBFIEUgBxgw',
259
+ 'C:0a12bd,' + Base64.strict_encode64("\xce\x0a\x12\xbd\x03\x01\x10\xff\x4b\x45\x51\x30\x37\x30\x34\x37\x35\x32\x24\x20\x3d\x09\x41\x20\x41\x20\x41\x20\x41\x20\x41\x20\x41\x20\x41\x20\x45\x20\x45\x20\x45\x20\x45\x20\x45\x20\x45\x20\x41\x20\x41\x20\x41\x20\x41\x20\x41\x20\x41\x20\x41\x20\x45\x20\x45\x20\x45\x20\x45\x20\x45\x20\x45\x20\x41\x20\x41\x20\x41\x20\x41\x20\x41\x20\x41\x20\x41\x20\x45\x20\x45\x20\x45\x20\x45\x20\x45\x20\x45\x20\x41\x20\x41\x20\x41\x20\x41\x20\x41\x20\x41\x20\x41\x20\x45\x20\x45\x20\x45\x20\x45\x20\x45\x20\x45\x20\x41\x20\x41\x20\x41\x20\x41\x20\x41\x20\x41\x20\x41\x20\x45\x20\x45\x20\x45\x20\x45\x20\x45\x20\x45\x20\x41\x20\x41\x20\x41\x20\x41\x20\x41\x20\x41\x20\x41\x20\x45\x20\x45\x20\x45\x20\x45\x20\x45\x20\x45\x20\x41\x20\x41\x20\x41\x20\x41\x20\x41\x20\x41\x20\x41\x20\x45\x20\x45\x20\x45\x20\x45\x20\x45\x20\x45\x20\x06\x18\xf4"),
260
+ # eco button
261
+ 'C:01b491,EQG0kQUAEg9KRVEwMzA1MjA1',
262
+ ]
263
+ end
264
+ let(:ret) do
265
+ [
266
+ { type: 'C', length: 237,
267
+ address: '03f6c9', rf_address: 0x03f6c9,
268
+ device_type: :cube,
269
+ firmware_version: '0113',
270
+ test_result: 255,
271
+ serial_number: 'JEQ0543545',
272
+ portal_enabled: true,
273
+ button_up_mode: :auto,
274
+ button_down_mode: :eco,
275
+ portal_url: "http://www.max-portal.elv.de:80/cube\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00".b,
276
+ },
277
+ { type: 'C', length: 237,
278
+ address: '10b199', rf_address: 0x10b199,
279
+ device_type: :cube,
280
+ firmware_version: '0113',
281
+ test_result: 255,
282
+ serial_number: 'LEQ1154727',
283
+ portal_enabled: false,
284
+ button_up_mode: :auto,
285
+ button_down_mode: :eco,
286
+ portal_url: "http://max.eq-3.de:80/cube\x000/lookup\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00".b,
287
+ },
288
+ { type: 'C', length: 237,
289
+ address: '03f25d', rf_address: 0x03f25d,
290
+ device_type: :cube,
291
+ firmware_version: '0113',
292
+ test_result: 0,
293
+ serial_number: 'JEQ0544923',
294
+ portal_enabled: false,
295
+ button_up_mode: :auto,
296
+ button_down_mode: :eco,
297
+ portal_url: "http://max.eq-3.de:80/cube\x000/lookup\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00".b,
298
+ },
299
+ { type: 'C', length: 210,
300
+ address: '0b04be', rf_address: 0x0b04be,
301
+ device_type: :radiator_thermostat_plus,
302
+ room_id: 1, _firmware_version: 16,
303
+ test_result: 255,
304
+ serial_number: 'KEQ0571674',
305
+ comfort_temperature: 22.0,
306
+ eco_temperature: 16.5,
307
+ max_setpoint_temperature: 30.5,
308
+ min_setpoint_temperature: 4.5,
309
+ temperature_offset: 0.0,
310
+ window_open_temperature: 12.0,
311
+ window_open_duration: 15,
312
+ boost_duration: 10,
313
+ valve_opening: 80,
314
+ decalcification_day: 'Wednesday',
315
+ decalcification_hour: 7,
316
+ max_valve_setting: 100.0,
317
+ valve_offset: 0.0,
318
+ },
319
+ { type: 'C', length: 210,
320
+ address: '08b6d2', rf_address: 0x08b6d2,
321
+ device_type: :radiator_thermostat_plus,
322
+ room_id: 2, _firmware_version: 16,
323
+ test_result: 0,
324
+ serial_number: 'KEQ0634607',
325
+ comfort_temperature: 23.0,
326
+ eco_temperature: 16.5,
327
+ max_setpoint_temperature: 30.5,
328
+ min_setpoint_temperature: 4.5,
329
+ temperature_offset: 0.0,
330
+ window_open_temperature: 12.0,
331
+ window_open_duration: 15,
332
+ boost_duration: 10,
333
+ valve_opening: 80,
334
+ decalcification_day: 'Saturday',
335
+ decalcification_hour: 12,
336
+ max_valve_setting: 100.0,
337
+ valve_offset: 0.0,
338
+ },
339
+ { type: 'C', length: 210,
340
+ address: '122b65', rf_address: 0x122b65,
341
+ device_type: :radiator_thermostat_plus,
342
+ room_id: 1, _firmware_version: 16,
343
+ test_result: 0,
344
+ serial_number: 'MEQ1472997',
345
+ comfort_temperature: 29.5,
346
+ eco_temperature: 19.5,
347
+ max_setpoint_temperature: 30.5,
348
+ min_setpoint_temperature: 4.5,
349
+ temperature_offset: 0.0,
350
+ window_open_temperature: 12.0,
351
+ window_open_duration: 15,
352
+ boost_duration: 20,
353
+ valve_opening: 90,
354
+ decalcification_day: 'Saturday',
355
+ decalcification_hour: 12,
356
+ max_valve_setting: 100.0,
357
+ valve_offset: 0.0,
358
+ },
359
+ { type: 'C', length: 210,
360
+ address: '06c941', rf_address: 0x06c941,
361
+ device_type: :radiator_thermostat,
362
+ room_id: 1, _firmware_version: 24,
363
+ test_result: 255,
364
+ serial_number: 'KEQ0352276',
365
+ comfort_temperature: 18.0,
366
+ eco_temperature: 16.0,
367
+ max_setpoint_temperature: 30.5,
368
+ min_setpoint_temperature: 4.5,
369
+ temperature_offset: 0.0,
370
+ window_open_temperature: 12.0,
371
+ window_open_duration: 15,
372
+ boost_duration: 60,
373
+ valve_opening: 100,
374
+ decalcification_day: 'Saturday',
375
+ decalcification_hour: 12,
376
+ max_valve_setting: 100.0,
377
+ valve_offset: 0.0,
378
+ },
379
+ { type: 'C', length: 206,
380
+ address: '121707', rf_address: 0x121707,
381
+ device_type: :wall_thermostat,
382
+ room_id: 1, _firmware_version: 16,
383
+ test_result: 255,
384
+ serial_number: 'MEQ0853603',
385
+ comfort_temperature: 21.5,
386
+ eco_temperature: 16.5,
387
+ max_setpoint_temperature: 30.5,
388
+ min_setpoint_temperature: 4.5,
389
+ unknown: "\x07\x18\x30".b,
390
+ },
391
+ { type: 'C', length: 206,
392
+ address: '0a12bd', rf_address: 0x0a12bd,
393
+ device_type: :wall_thermostat,
394
+ room_id: 1, _firmware_version: 16,
395
+ test_result: 255,
396
+ serial_number: 'KEQ0704752',
397
+ comfort_temperature: 18.0,
398
+ eco_temperature: 16.0,
399
+ max_setpoint_temperature: 30.5,
400
+ min_setpoint_temperature: 4.5,
401
+ unknown: "\x06\x18\xf4".b,
402
+ },
403
+ { type: 'C', length: 17,
404
+ address: '01b491', rf_address: 0x01b491,
405
+ device_type: :eco_switch,
406
+ room_id: 0, _firmware_version: 18,
407
+ test_result: 15,
408
+ serial_number: 'JEQ0305205',
409
+ },
410
+ ]
411
+ end
412
+ it 'returns proper hash' do
413
+ msgs.each_with_index do |m, i|
414
+ expect(parser.parse_tcp_msg(m)
415
+ .delete_if do |k, _|
416
+ [
417
+ :unknown1, :unknown2, :unknown3, :unknown4,
418
+ # :_timezone_winter, :_timezone_winter_offset,
419
+ # :timezone_winter_day, :timezone_winter_month,
420
+ # :_timezone_daylight, :_timezone_daylight_offset,
421
+ # :timezone_daylight_day, :timezone_daylight_month,
422
+ :weekly_program
423
+ ].include?(k)
424
+ end).to eq(ret[i])
425
+ end
426
+ end
427
+ end
428
+ end
429
+
430
+ # F:ntp.homematic.com,ntp.homematic.com
431
+ describe 'F message' do
432
+ let(:msgs) do
433
+ [
434
+ 'F:',
435
+ 'F:ntp.homematic.com',
436
+ 'F:ntp.homematic.com,',
437
+ 'F:ntp.homematic.com,ntp.homematic.com',
438
+ ]
439
+ end
440
+ let(:ret) do
441
+ [
442
+ { type: 'F', ntp_servers: [] },
443
+ { type: 'F', ntp_servers: ['ntp.homematic.com'] },
444
+ { type: 'F', ntp_servers: ['ntp.homematic.com'] },
445
+ { type: 'F', ntp_servers: ['ntp.homematic.com', 'ntp.homematic.com'] },
446
+ ]
447
+ end
448
+ it 'returns proper hash' do
449
+ msgs.each_with_index do |m, i|
450
+ expect(parser.parse_tcp_msg(m)).to eq(ret[i])
451
+ end
452
+ end
453
+ end
454
+
455
+ # H:KEQ0523864,097f2c,0113,00000000,477719c0,00,32,0d0c09,1404,03,0000
456
+ describe 'H message' do
457
+ context 'invalid message body' do
458
+ let(:msgs) do
459
+ [
460
+ # invalid lengths of message parts
461
+ 'H:',
462
+ 'H:,,,,,,,,,,',
463
+ 'H:EQ0523864,097f2c,0113,00000000,477719c0,00,32,0d0c09,1404,03,0000',
464
+ 'H:IKEQ0523864,097f2c,0113,00000000,477719c0,00,32,0d0c09,1404,03,0000',
465
+ 'H:IEQ0523864,97f2c,0113,00000000,477719c0,00,32,0d0c09,1404,03,0000',
466
+ 'H:IKEQ0523864,97f2c,0113,00000000,477719c0,00,32,0d0c09,1404,03,0000',
467
+ 'H:IEQ0523864,197f2c,113,00000000,477719c0,00,32,0d0c09,1404,03,0000',
468
+ 'H:IKEQ0523864,197f2c,113,00000000,477719c0,00,32,0d0c09,1404,03,0000',
469
+ 'H:IKEQ0523864,197f2c,0113,0000000,477719c0,00,32,0d0c09,1404,03,0000',
470
+ 'H:IKEQ0523864,197f2c,0113,00000000,77719c0,00,32,0d0c09,1404,03,0000',
471
+ 'H:IKEQ0523864,197f2c,0113,00000000,477719c0,0,32,0d0c09,1404,03,0000',
472
+ 'H:IKEQ0523864,197f2c,0113,00000000,477719c0,00,2,0d0c09,1404,03,0000',
473
+ 'H:IKEQ0523864,197f2c,0113,00000000,477719c0,00,32,d0c09,1404,03,0000',
474
+ 'H:IKEQ0523864,197f2c,0113,00000000,477719c0,00,32,0d0c09,404,03,0000',
475
+ 'H:IKEQ0523864,197f2c,0113,00000000,477719c0,00,32,0d0c09,1404,3,0000',
476
+ 'H:IKEQ0523864,197f2c,0113,00000000,477719c0,00,32,0d0c09,1404,03,000',
477
+ # invalid hex format
478
+ 'H:KEQ0523864,x97f2c,0113,00000000,477719c0,00,32,0d0c09,1404,03,0000',
479
+ 'H:KEQ0523864,097f2c,x113,00000000,477719c0,00,32,0d0c09,1404,03,0000',
480
+ 'H:KEQ0523864,097f2c,0113,00000000,477719cx,00,32,0d0c09,1404,03,0000',
481
+ 'H:KEQ0523864,097f2c,0113,00000000,477719c0,x0,32,0d0c09,1404,03,0000',
482
+ 'H:KEQ0523864,097f2c,0113,00000000,477719c0,00,3g,0d0c09,1404,03,0000',
483
+ ]
484
+ end
485
+ it 'raises proper exception' do
486
+ msgs.each do |m|
487
+ expect{ parser.parse_tcp_msg(m) }.to raise_error Messages::InvalidMessageBody
488
+ end
489
+ end
490
+ end
491
+
492
+ context 'valid message body' do
493
+ let(:msgs) do
494
+ [
495
+ 'H:KEQ0523864,097f2c,0113,00000000,477719c0,00,32,0d0c09,1404,03,0000',
496
+ 'H:4KFK49VMD6,c233fe,211f,00000000,478819c0,16,3c,000a03,1024,03,00fd',
497
+ 'H:JEQ0544923,03f25d,0113,00000000,299ca43f,00,32,0d0c1d,1013,03,0000',
498
+ 'H:JEQ0543545,03f6c9,0113,00000000,2663651e,00,32,0d0c0f,000c,03,0000',
499
+
500
+ ]
501
+ end
502
+ let(:ret) do
503
+ [
504
+ { type: 'H', serial_number: 'KEQ0523864', rf_address: 0x097f2c,
505
+ firmware_version: '0113', unknown: '00000000',
506
+ http_id: 0x477719c0, duty_cycle: 0, free_memory_slots: 50,
507
+ cube_datetime: Time.new(2013, 12, 9, 20, 4),
508
+ state_cube_time: 3, ntp_counter: 0 },
509
+ { type: 'H', serial_number: '4KFK49VMD6', rf_address: 0xc233fe,
510
+ firmware_version: '211f', unknown: '00000000',
511
+ http_id: 0x478819c0, duty_cycle: 22, free_memory_slots: 60,
512
+ cube_datetime: Time.new(2000, 10, 3, 16, 36),
513
+ state_cube_time: 3, ntp_counter: 253 },
514
+ { type: 'H', serial_number: 'JEQ0544923', rf_address: 0x03f25d,
515
+ firmware_version: '0113', unknown: '00000000',
516
+ http_id: 0x299ca43f, duty_cycle: 0, free_memory_slots: 50,
517
+ cube_datetime: Time.new(2013, 12, 29, 16, 19),
518
+ state_cube_time: 3, ntp_counter: 0 },
519
+ { type: 'H', serial_number: 'JEQ0543545', rf_address: 0x03f6c9,
520
+ firmware_version: '0113', unknown: '00000000',
521
+ http_id: 0x2663651e, duty_cycle: 0, free_memory_slots: 50,
522
+ cube_datetime: Time.new(2013, 12, 15, 0, 12),
523
+ state_cube_time: 3, ntp_counter: 0 },
524
+ ]
525
+ end
526
+ it 'returns proper hash' do
527
+ msgs.each_with_index do |m, i|
528
+ expect(parser.parse_tcp_msg(m)).to eq(ret[i])
529
+ end
530
+ end
531
+ end
532
+ end
533
+
534
+ # L:Cw/a7QkSGBgoAMwACw/DcwkSGBgoAM8ACw/DgAkSGBgoAM4A
535
+ describe 'L message' do
536
+ context 'invalid message body' do
537
+ let(:msgs) do
538
+ [
539
+ # unexpected EOF
540
+ 'L:' + Base64.strict_encode64("\x06\x0f\xda\xed\x09\x00"),
541
+ 'L:' + Base64.strict_encode64("\x0b\x0f\xda\xed\x09\x00\x00\x18\x18\x01\x00"),
542
+ 'L:' + Base64.strict_encode64("\x0c\x0f\xda\xed\x09\x12\x18\x18\xa8\x9d\x0b\x04"),
543
+ # invalid length of submessage
544
+ 'L:' + Base64.strict_encode64("\x00"),
545
+ 'L:' + Base64.strict_encode64("\x03\x0f\xda\xed"),
546
+ 'L:' + Base64.strict_encode64("\x05\x0f\xda\xed\x09\x00\x00"),
547
+ 'L:' + Base64.strict_encode64("\x07\x0f\xda\xed\x09\x00\x00"),
548
+ 'L:' + Base64.strict_encode64("\x07\x0f\xda\xed\x09\x00\x00\x00"),
549
+ 'L:' + Base64.strict_encode64("\x07\x0f\xda\xed\x09\x00\x00\x18"),
550
+ 'L:' + Base64.strict_encode64("\x08\x0f\xda\xed\x09\x00\x00\x18"),
551
+ 'L:' + Base64.strict_encode64("\x0a\x0f\xda\xed\x09\x00\x00\x18\x18\x01\x00"),
552
+ 'L:' + Base64.strict_encode64("\x0a\x0f\xda\xed\x09\x00\x00\x18\x18\x01\x00\x00"),
553
+ 'L:' + Base64.strict_encode64("\x0d\x0f\xda\xed\x09\x00\x00\x18\x18\x01\x00\x00\x00\x00"),
554
+ 'L:' + Base64.strict_encode64("\x0b\x0f\xda\xed\x09\x00\x00\x18\x18\x01\x00\x00\x00"),
555
+ # 'date_until' part is not valid date and 'actual_temperature' is currently present at offset 12
556
+ 'L:' + Base64.strict_encode64("\x0c\x0f\xda\xed\x09\x00\x00\x18\x18\x01\x00\x00\x00"),
557
+ 'L:' + Base64.strict_encode64("\x0c\x0f\xda\xed\x09\x00\x00\x18\xa8\x00\x00\x04\x24"),
558
+ # 'date_until' part is not valid date and 'mode' is not 'auto'
559
+ 'L:' + Base64.strict_encode64("\x0b\x0f\xda\xed\x09\x00\x01\x18\xa8\x00\x00\x04"),
560
+ ]
561
+ end
562
+ it 'raises proper exception' do
563
+ msgs.each do |m|
564
+ expect{ parser.parse_tcp_msg(m) }.to raise_error Messages::InvalidMessageBody
565
+ end
566
+ end
567
+ end
568
+
569
+ context 'valid message body' do
570
+ let(:msgs) do
571
+ [
572
+ 'L:',
573
+ 'L:' + Base64.strict_encode64("\x06\x0f\xda\xed\x09\x00\x03"),
574
+ 'L:' + Base64.strict_encode64("\x0b\x0f\xda\xed\x09\x00\x00\x18\x18\x01\x00\x00"),
575
+ 'L:' + Base64.strict_encode64("\x0c\x0f\xda\xed\x09\x12\x18\x18\xa8\x9d\x0b\x04\x24"),
576
+ 'L:' + Base64.strict_encode64("\x0b\x0f\xda\xed\x09\x12\x18\x18\xa8\xbd\xef\x04"),
577
+ 'L:' + Base64.strict_encode64("\x0b\x0f\xda\xed\x09\x12\x18\x18\xa8\xbd\xf0\x05"),
578
+ 'L:Cw/a7QkSGBgoAMwACw/DcwkSGBgoAM8ACw/DgAkSGBgoAM4A',
579
+ 'L:CwsEvvYSGAAiANsACwi20lwSGAAiAOEABgG0kVwSEF==',
580
+
581
+ ]
582
+ end
583
+ let(:ret) do
584
+ [
585
+ { type: 'L', devices: [] },
586
+ { type: 'L', devices: [
587
+ { length: 6, rf_address: 0x0fdaed, unknown: "\x09".b,
588
+ flags: {
589
+ value: 0x0003,
590
+ mode: :boost,
591
+ dst_setting_active: false,
592
+ gateway_known: false,
593
+ panel_locked: false,
594
+ link_error: false,
595
+ low_battery: false,
596
+ status_initialized: false,
597
+ is_answer: false,
598
+ error: false,
599
+ valid_info: false,
600
+ },
601
+ },
602
+ ],
603
+ },
604
+ { type: 'L', devices: [
605
+ { length: 11, rf_address: 0x0fdaed, unknown: "\x09".b,
606
+ flags: {
607
+ value: 0x0000,
608
+ mode: :auto,
609
+ dst_setting_active: false,
610
+ gateway_known: false,
611
+ panel_locked: false,
612
+ link_error: false,
613
+ low_battery: false,
614
+ status_initialized: false,
615
+ is_answer: false,
616
+ error: false,
617
+ valid_info: false,
618
+ },
619
+ valve_opening: 24, temperature: 12.0,
620
+ actual_temperature: 25.6,
621
+ },
622
+ ],
623
+ },
624
+ { type: 'L', devices: [
625
+ { length: 12, rf_address: 0x0fdaed, unknown: "\x09".b,
626
+ flags: {
627
+ value: 0x1218,
628
+ mode: :auto,
629
+ dst_setting_active: true,
630
+ gateway_known: true,
631
+ panel_locked: false,
632
+ link_error: false,
633
+ low_battery: false,
634
+ status_initialized: true,
635
+ is_answer: false,
636
+ error: false,
637
+ valid_info: true,
638
+ },
639
+ valve_opening: 24, temperature: 20.0,
640
+ datetime_until: Time.new(2011, 8, 29, 2, 0),
641
+ actual_temperature: 29.2,
642
+ },
643
+ ],
644
+ },
645
+ { type: 'L', devices: [
646
+ { length: 11, rf_address: 0x0fdaed, unknown: "\x09".b,
647
+ flags: {
648
+ value: 0x1218,
649
+ mode: :auto,
650
+ dst_setting_active: true,
651
+ gateway_known: true,
652
+ panel_locked: false,
653
+ link_error: false,
654
+ low_battery: false,
655
+ status_initialized: true,
656
+ is_answer: false,
657
+ error: false,
658
+ valid_info: true,
659
+ },
660
+ valve_opening: 24, temperature: 20.0,
661
+ datetime_until: Time.new(2015, 11, 29, 2, 0),
662
+ },
663
+ ],
664
+ },
665
+ { type: 'L', devices: [
666
+ { length: 11, rf_address: 0x0fdaed, unknown: "\x09".b,
667
+ flags: {
668
+ value: 0x1218,
669
+ mode: :auto,
670
+ dst_setting_active: true,
671
+ gateway_known: true,
672
+ panel_locked: false,
673
+ link_error: false,
674
+ low_battery: false,
675
+ status_initialized: true,
676
+ is_answer: false,
677
+ error: false,
678
+ valid_info: true,
679
+ },
680
+ valve_opening: 24, temperature: 20.0,
681
+ datetime_until: Time.new(2016, 11, 29, 2, 30),
682
+ },
683
+ ],
684
+ },
685
+ { type: 'L', devices: [
686
+ {
687
+ length: 11, rf_address: 0x0fdaed, unknown: "\x09".b,
688
+ flags: {
689
+ value: 0x1218,
690
+ mode: :auto,
691
+ dst_setting_active: true,
692
+ gateway_known: true,
693
+ panel_locked: false,
694
+ link_error: false,
695
+ low_battery: false,
696
+ status_initialized: true,
697
+ is_answer: false,
698
+ error: false,
699
+ valid_info: true,
700
+ },
701
+ valve_opening: 24, temperature: 20.0,
702
+ actual_temperature: 20.4,
703
+ },
704
+ {
705
+ length: 11, rf_address: 0x0fc373, unknown: "\x09".b,
706
+ flags: {
707
+ value: 0x1218,
708
+ mode: :auto,
709
+ dst_setting_active: true,
710
+ gateway_known: true,
711
+ panel_locked: false,
712
+ link_error: false,
713
+ low_battery: false,
714
+ status_initialized: true,
715
+ is_answer: false,
716
+ error: false,
717
+ valid_info: true,
718
+ },
719
+ valve_opening: 24, temperature: 20.0,
720
+ actual_temperature: 20.7,
721
+ },
722
+ {
723
+ length: 11, rf_address: 0x0fc380, unknown: "\x09".b,
724
+ flags: {
725
+ value: 0x1218,
726
+ mode: :auto,
727
+ dst_setting_active: true,
728
+ gateway_known: true,
729
+ panel_locked: false,
730
+ link_error: false,
731
+ low_battery: false,
732
+ status_initialized: true,
733
+ is_answer: false,
734
+ error: false,
735
+ valid_info: true,
736
+ },
737
+ valve_opening: 24, temperature: 20.0,
738
+ actual_temperature: 20.6,
739
+ },
740
+ ],
741
+ },
742
+ { type: 'L', devices: [
743
+ {
744
+ length: 11, rf_address: 0x0b04be, unknown: "\xf6".b,
745
+ flags: {
746
+ value: 0x1218,
747
+ mode: :auto,
748
+ dst_setting_active: true,
749
+ gateway_known: true,
750
+ panel_locked: false,
751
+ link_error: false,
752
+ low_battery: false,
753
+ status_initialized: true,
754
+ is_answer: false,
755
+ error: false,
756
+ valid_info: true,
757
+ },
758
+ valve_opening: 0, temperature: 17.0,
759
+ actual_temperature: 21.9,
760
+ },
761
+ {
762
+ length: 11, rf_address: 0x08b6d2, unknown: "\x5c".b,
763
+ flags: {
764
+ value: 0x1218,
765
+ mode: :auto,
766
+ dst_setting_active: true,
767
+ gateway_known: true,
768
+ panel_locked: false,
769
+ link_error: false,
770
+ low_battery: false,
771
+ status_initialized: true,
772
+ is_answer: false,
773
+ error: false,
774
+ valid_info: true,
775
+ },
776
+ valve_opening: 0, temperature: 17.0,
777
+ actual_temperature: 22.5,
778
+ },
779
+ {
780
+ length: 6, rf_address: 0x01b491, unknown: "\x5c".b,
781
+ flags: {
782
+ value: 0x1210,
783
+ mode: :auto,
784
+ dst_setting_active: false,
785
+ gateway_known: true,
786
+ panel_locked: false,
787
+ link_error: false,
788
+ low_battery: false,
789
+ status_initialized: true,
790
+ is_answer: false,
791
+ error: false,
792
+ valid_info: true,
793
+ },
794
+ },
795
+ ],
796
+ },
797
+ ]
798
+ end
799
+ it 'returns proper hash' do
800
+ msgs.each_with_index do |m, i|
801
+ expect(parser.parse_tcp_msg(m)).to eq(ret[i])
802
+ end
803
+ end
804
+ end
805
+ end
806
+
807
+ # M:00,01,VgIEAQNCYWQK7WkCBEJ1cm8K8wADCldvaG56aW1tZXIK8wwEDFNjaGxhZnppbW1lcgr
808
+ # 1QAUCCu1pS0VRMDM3ODA0MAZIVCBCYWQBAgrzAEtFUTAzNzk1NDQHSFQgQnVybwICCvMMS0VRMD
809
+ # M3OTU1NhlIVCBXb2huemltbWVyIEJhbGtvbnNlaXRlAwIK83lLRVEwMzc5NjY1GkhUIFdvaG56a
810
+ # W1tZXIgRmVuc3RlcnNlaXRlAwIK9UBLRVEwMzgwMTIwD0hUIFNjaGxhZnppbW1lcgQB
811
+ describe 'M message' do
812
+ context 'invalid message body' do
813
+ let(:msgs) do
814
+ [
815
+ # message parts lengths are too short
816
+ 'M:00',
817
+ 'M:00,',
818
+ 'M:00,,',
819
+ 'M:00,01',
820
+ 'M:00,01,',
821
+ 'M:00,01,' + Base64.strict_encode64('a'),
822
+ 'M:00,01,' + Base64.strict_encode64('ab1'),
823
+ 'M:00,01,' + Base64.strict_encode64('ab01'),
824
+ 'M:00,01,' + Base64.strict_encode64('ab192XY123'),
825
+ 'M:00,01,' + Base64.strict_encode64("ab\x01"),
826
+ 'M:00,01,' + Base64.strict_encode64("ab\x00\x01"),
827
+ 'M:00,01,' + Base64.strict_encode64("ab\x01!\x02XY123"),
828
+ 'M:00,01,' + Base64.strict_encode64("ab\x01!\x02XY123\x01"),
829
+ 'M:00,01,' + Base64.strict_encode64("ab\x01!\x02XY123\x01\x04RFAserial_num\x04NAME"),
830
+ 'M:00,01,' + Base64.strict_encode64("ab\x01!\x02XY123\x02\x04RFAserial_num\x04NAME!"),
831
+ # index/count not hex
832
+ 'M:xx,01,' + Base64.strict_encode64("ab\x00\x00"),
833
+ 'M:00,xx,' + Base64.strict_encode64("ab\x00\x00"),
834
+ # index >= count
835
+ 'M:01,01,' + Base64.strict_encode64("ab\x00\x00"),
836
+ # invalid device type
837
+ 'M:00,01,' + Base64.strict_encode64("Vx\x01!\x02XY123\x01\x07RFAserial_num\x04NAME!"),
838
+ ]
839
+ end
840
+ it 'raises proper exception' do
841
+ msgs.each do |m|
842
+ expect{ parser.parse_tcp_msg(m) }.to raise_error Messages::InvalidMessageBody
843
+ end
844
+ end
845
+ end
846
+
847
+ context 'valid message body' do
848
+ let(:msgs) do
849
+ [
850
+ 'M:00,01,' + Base64.strict_encode64("Vx\x01!\x02XY123\x01\x04RFAserial_num\x04NAME!\x01"),
851
+ 'M:00,01,VgIEAQNCYWQK7WkCBEJ1cm8K8wADCldvaG56aW1tZXIK8wwEDFNjaGxhZnppbW1lcgr' \
852
+ '1QAUCCu1pS0VRMDM3ODA0MAZIVCBCYWQBAgrzAEtFUTAzNzk1NDQHSFQgQnVybwICCvMMS0VRMD' \
853
+ 'M3OTU1NhlIVCBXb2huemltbWVyIEJhbGtvbnNlaXRlAwIK83lLRVEwMzc5NjY1GkhUIFdvaG56a' \
854
+ 'W1tZXIgRmVuc3RlcnNlaXRlAwIK9UBLRVEwMzgwMTIwD0hUIFNjaGxhZnppbW1lcgQB',
855
+ 'M:00,01,VgICAg1PYnl2YWNpIHBva29qCLbSAQdQcmVkc2luCwS+AwILBL5LRVEwNTcxNjc0C3RvcGVuaSB1IHdjAQIIttJLRVEwNjM0NjA3CVBvZCBva25lbQIFAbSRSkVRMDMwNTIwNQpFY28gU3dpdGNoAAE=',
856
+ ]
857
+ end
858
+ let(:ret) do
859
+ [
860
+ {
861
+ type: 'M', index: 0, count: 1, unknown1: 'Vx', unknown2: "\x01".b,
862
+ rooms_count: 1, rooms: [
863
+ { id: '!'.unpack1('C'), name_length: 2, name: 'XY', rf_address: 0x313233, },
864
+ ],
865
+ devices_count: 1, devices: [
866
+ { type: :shutter_contact, rf_address: 0x524641, serial_number: 'serial_num', name_length: 4, name: 'NAME', room_id: '!'.unpack1('C') },
867
+ ],
868
+ },
869
+ {
870
+ type: 'M', index: 0, count: 1, unknown1: "V\x02".b, unknown2: "\x01".b,
871
+ rooms_count: 4, rooms: [
872
+ { id: 1, name_length: 3, name: 'Bad', rf_address: 0x0aed69, },
873
+ { id: 2, name_length: 4, name: 'Buro', rf_address: 0x0af300, },
874
+ { id: 3, name_length: 10, name: 'Wohnzimmer', rf_address: 0x0af30c, },
875
+ { id: 4, name_length: 12, name: 'Schlafzimmer', rf_address: 0x0af540, },
876
+ ],
877
+ devices_count: 5, devices: [
878
+ { type: :radiator_thermostat_plus, rf_address: 0x0aed69, serial_number: 'KEQ0378040', name_length: 6, name: 'HT Bad', room_id: 1, },
879
+ { type: :radiator_thermostat_plus, rf_address: 0x0af300, serial_number: 'KEQ0379544', name_length: 7, name: 'HT Buro', room_id: 2, },
880
+ { type: :radiator_thermostat_plus, rf_address: 0x0af30c, serial_number: 'KEQ0379556', name_length: 25, name: 'HT Wohnzimmer Balkonseite', room_id: 3, },
881
+ { type: :radiator_thermostat_plus, rf_address: 0x0af379, serial_number: 'KEQ0379665', name_length: 26, name: 'HT Wohnzimmer Fensterseite', room_id: 3, },
882
+ { type: :radiator_thermostat_plus, rf_address: 0x0af540, serial_number: 'KEQ0380120', name_length: 15, name: 'HT Schlafzimmer', room_id: 4, },
883
+ ],
884
+ },
885
+ {
886
+ type: 'M', index: 0, count: 1, unknown1: "V\x02".b, unknown2: "\x01".b,
887
+ rooms_count: 2, rooms: [
888
+ { id: 2, name_length: 13, name: 'Obyvaci pokoj', rf_address: 0x08b6d2, },
889
+ { id: 1, name_length: 7, name: 'Predsin', rf_address: 0x0b04be, },
890
+ ],
891
+ devices_count: 3, devices: [
892
+ { type: :radiator_thermostat_plus, rf_address: 0x0b04be, serial_number: 'KEQ0571674', name_length: 11, name: 'topeni u wc', room_id: 1, },
893
+ { type: :radiator_thermostat_plus, rf_address: 0x08b6d2, serial_number: 'KEQ0634607', name_length: 9, name: 'Pod oknem', room_id: 2, },
894
+ { type: :eco_switch, rf_address: 0x01b491, serial_number: 'JEQ0305205', name_length: 10, name: 'Eco Switch', room_id: 0, },
895
+ ],
896
+ },
897
+ ]
898
+ end
899
+ it 'returns proper hash' do
900
+ msgs.each_with_index do |m, i|
901
+ expect(parser.parse_tcp_msg(m)).to eq(ret[i])
902
+ end
903
+ end
904
+ end
905
+ end
906
+
907
+ # N:Aw4VzExFUTAwMTUzNDD/
908
+ describe 'N message' do
909
+ context 'invalid message body' do
910
+ let(:msgs) do
911
+ [
912
+ # invalid device type
913
+ 'N:' + Base64.strict_encode64("\x06\x0e\x15\xccLEQ0015340\xff"),
914
+ # invalid length
915
+ 'N:' + Base64.strict_encode64("\x00\x0e\x15\xccLEQ0015340"),
916
+ ]
917
+ end
918
+ it 'raises proper exception' do
919
+ msgs.each do |m|
920
+ expect{ parser.parse_tcp_msg(m) }.to raise_error Messages::InvalidMessageBody
921
+ end
922
+ end
923
+ end
924
+
925
+ context 'valid message body' do
926
+ let(:msgs) do
927
+ [
928
+ 'N:Aw4VzExFUTAwMTUzNDD/',
929
+ ]
930
+ end
931
+ let(:ret) do
932
+ [
933
+ { type: 'N', device_type: :wall_thermostat,
934
+ rf_address: 0x0e15cc,
935
+ serial_number: 'LEQ0015340',
936
+ unknown: "\xff".b,
937
+ },
938
+ ]
939
+ end
940
+ it 'returns proper hash' do
941
+ msgs.each_with_index do |m, i|
942
+ expect(parser.parse_tcp_msg(m)).to eq(ret[i])
943
+ end
944
+ end
945
+ end
946
+ end
947
+
948
+ # S:00,0,31
949
+ describe 'S message' do
950
+ context 'invalid message body' do
951
+ let(:msgs) do
952
+ [
953
+ # invalid lengths of message parts
954
+ 'S:',
955
+ 'S:0,0,1',
956
+ 'S:00,0',
957
+ 'S:0,0,31',
958
+ 'S:00,,31',
959
+ # invalid hex format
960
+ 'S:0x,0,31',
961
+ 'S:00,s,31',
962
+ 'S:00,0,3g',
963
+ ]
964
+ end
965
+ it 'raises proper exception' do
966
+ msgs.each do |m|
967
+ expect{ parser.parse_tcp_msg(m) }.to raise_error Messages::InvalidMessageBody
968
+ end
969
+ end
970
+ end
971
+
972
+ context 'valid message body' do
973
+ let(:msgs) do
974
+ [
975
+ 'S:00,0,31',
976
+ 'S:00,1,31',
977
+ 'S:63,2,fd',
978
+ ]
979
+ end
980
+ let(:ret) do
981
+ [
982
+ { type: 'S',
983
+ duty_cycle: 0, command_processed: true,
984
+ free_memory_slots: 49 },
985
+ { type: 'S',
986
+ duty_cycle: 0, command_processed: false,
987
+ free_memory_slots: 49 },
988
+ { type: 'S',
989
+ duty_cycle: 99, command_processed: false,
990
+ free_memory_slots: 253 },
991
+ ]
992
+ end
993
+ it 'returns proper hash' do
994
+ msgs.each_with_index do |m, i|
995
+ expect(parser.parse_tcp_msg(m)).to eq(ret[i])
996
+ end
997
+ end
998
+ end
999
+ end
1000
+
1001
+ end
1002
+ end
1003
+
1004
+ describe 'TCP::Serializer' do
1005
+ subject(:serializer) { Messages::TCP::Serializer.new }
1006
+
1007
+ # Proper message examples:
1008
+ # a:\r\n
1009
+ # n:003c\r\n
1010
+ # q:\r\n
1011
+ # t:01,1,Dx1U\r\n
1012
+
1013
+ describe 'invalid data' do
1014
+ context 'empty data' do
1015
+ let(:inputs) do
1016
+ [
1017
+ [],
1018
+ ]
1019
+ end
1020
+ it 'returns empty data' do
1021
+ inputs.each do |i|
1022
+ expect(serializer.serialize_tcp_hashes(i)).to eq('')
1023
+ end
1024
+ end
1025
+ end
1026
+
1027
+ context 'invalid message type' do
1028
+ let(:hashes) do
1029
+ [
1030
+ {},
1031
+ { type_: 'a', },
1032
+ { _type: 'a', },
1033
+ { type: 'aa' },
1034
+ { type: 'X' },
1035
+ { type: 'A' },
1036
+ ]
1037
+ end
1038
+ it 'raises proper exception and #valid_hash_msg_type and #valid_tcp_hash are falsey' do
1039
+ hashes.each do |h|
1040
+ expect{ serializer.serialize_tcp_hash(h) }.to raise_error Messages::InvalidMessageType
1041
+ expect(serializer.valid_hash_msg_type(h)).to be_falsey
1042
+ expect(serializer.valid_tcp_hash(h)).to be_falsey
1043
+ end
1044
+ end
1045
+ it 'raises proper exception when passed as array of hashes' do
1046
+ hashes.each do |h|
1047
+ expect{ serializer.serialize_tcp_hashes([h]) }.to raise_error Messages::InvalidMessageType
1048
+ end
1049
+ end
1050
+ end
1051
+
1052
+ context 'invalid hash keys' do
1053
+ let(:hashes) do
1054
+ [
1055
+ { type: 'm' },
1056
+ { type: 'm', data: 'data' },
1057
+ { type: 'm', data: 'data', rooms_count: 1, rooms: [],
1058
+ devices_count: 1, devices: [], },
1059
+ { type: 'a', data: 'data' },
1060
+ { type: 'a', data: nil },
1061
+ ]
1062
+ end
1063
+ it 'raises proper exception and #valid_hash_keys and #valid_tcp_hash are falsey' do
1064
+ hashes.each do |h|
1065
+ expect{ serializer.serialize_tcp_hash(h) }.to raise_error Messages::InvalidMessageBody
1066
+ expect(serializer.valid_hash_keys(h)).to be_falsey
1067
+ expect(serializer.valid_tcp_hash(h)).to be_falsey
1068
+ end
1069
+ end
1070
+ it 'raises proper exception when passed as array of hashes' do
1071
+ hashes.each do |h|
1072
+ expect{ serializer.serialize_tcp_hashes([h]) }.to raise_error Messages::InvalidMessageBody
1073
+ end
1074
+ end
1075
+ end
1076
+
1077
+ context 'invalid hash values' do
1078
+ let(:hashes) do
1079
+ [
1080
+ { type: 'u', url: 'URL', port: nil },
1081
+ { type: 'u', url: nil, port: 80 },
1082
+ ]
1083
+ end
1084
+ it 'raises proper exception and #valid_hash_values and #valid_tcp_hash are falsey' do
1085
+ hashes.each do |h|
1086
+ expect{ serializer.serialize_tcp_hash(h) }.to raise_error Messages::InvalidMessageBody
1087
+ expect(serializer.valid_hash_values(h)).to be_falsey
1088
+ expect(serializer.valid_tcp_hash(h)).to be_falsey
1089
+ end
1090
+ end
1091
+ it 'raises proper exception when passed as array of hashes' do
1092
+ hashes.each do |h|
1093
+ expect{ serializer.serialize_tcp_hashes([h]) }.to raise_error Messages::InvalidMessageBody
1094
+ end
1095
+ end
1096
+ end
1097
+
1098
+ context 'valid message type but serializer not implemented yet' do
1099
+ let(:hashes) do
1100
+ [
1101
+ { type: 'g' },
1102
+ { type: 'W' },
1103
+ { type: 'r' },
1104
+ ]
1105
+ end
1106
+ it 'raises proper exception' do
1107
+ hashes.each do |h|
1108
+ expect{ serializer.serialize_tcp_hash(h) }.to raise_error Messages::InvalidMessageType
1109
+ end
1110
+ end
1111
+ end
1112
+
1113
+ # ! Check whether output data is valid: length is not too long, format is OK, ..
1114
+
1115
+ end
1116
+
1117
+ describe 'valid data' do
1118
+ let(:data) do
1119
+ [
1120
+ [{ type: 'a'}, ],
1121
+ [{ type: 'a'}, { type: 'a'}, ],
1122
+ [{ type: 'a'}, { type: 'l'}, { type: 'a'}, ],
1123
+ [{ type: 'l'}, { type: 'l'}, { type: 'a'}, ],
1124
+ [{ type: 'q'}, { type: 'l'}, { type: 'a'}, ],
1125
+ ]
1126
+ end
1127
+ let(:res) do
1128
+ [
1129
+ %w[a],
1130
+ %w[a a],
1131
+ %w[a l a],
1132
+ %w[l l a],
1133
+ %w[q l a],
1134
+ ]
1135
+ end
1136
+ it 'returns string with valid format and proper message types' do
1137
+ data.each_with_index do |d, i|
1138
+ data = serializer.serialize_tcp_hashes(d)
1139
+ expect(data).to be_a(String)
1140
+ expect(serializer.valid_tcp_data(data)).to be_truthy
1141
+ expect(data[0]).to eq(res[i][0])
1142
+ data.split("\r\n").each_with_index do |m, j|
1143
+ expect(serializer.valid_tcp_msg(m)).to be_truthy
1144
+ expect(m[0]).to eq(res[i][j])
1145
+ end
1146
+ end
1147
+ end
1148
+ end
1149
+
1150
+ describe 'concrete message types' do
1151
+
1152
+ describe 'a, c, l, q messages' do
1153
+ let(:types) { %w[a c l q] }
1154
+ context 'invalid hash' do
1155
+ let(:hashes) do
1156
+ [
1157
+ { unknown: 'something' },
1158
+ { data: 'super interesting' },
1159
+ ]
1160
+ end
1161
+ it 'raises proper exception' do
1162
+ types.each do |t|
1163
+ hashes.each do |h|
1164
+ h[:type] = t
1165
+ expect{ serializer.serialize_tcp_hash(h) }.to raise_error Messages::InvalidMessageBody
1166
+ end
1167
+ end
1168
+ end
1169
+ end
1170
+ context 'valid hash' do
1171
+ let(:hashes) do
1172
+ [
1173
+ {},
1174
+ ]
1175
+ end
1176
+ it 'returns proper string (without any additional content)' do
1177
+ types.each do |t|
1178
+ hashes.each do |h|
1179
+ h[:type] = t
1180
+ expect(serializer.serialize_tcp_hash(h)).to eq("#{t}:\r\n")
1181
+ end
1182
+ end
1183
+ end
1184
+ end
1185
+ end
1186
+
1187
+ # f:
1188
+ # f:nl.pool.ntp.org,ntp.homematic.com
1189
+ describe 'f message' do
1190
+ context 'valid hash' do
1191
+ let(:hashes) do
1192
+ [
1193
+ { type: 'f', },
1194
+ { type: 'f', ntp_servers: [], },
1195
+ { type: 'f', ntp_servers: ['nl.pool.ntp.org'], },
1196
+ { type: 'f', ntp_servers: ['nl.pool.ntp.org', 'ntp.homematic.com'], },
1197
+ ]
1198
+ end
1199
+ let(:ret) do
1200
+ [
1201
+ 'f:',
1202
+ 'f:',
1203
+ 'f:nl.pool.ntp.org',
1204
+ 'f:nl.pool.ntp.org,ntp.homematic.com',
1205
+ ].map { |s| s << "\r\n" }
1206
+ end
1207
+ it 'returns proper string' do
1208
+ hashes.each_with_index do |h, i|
1209
+ expect(serializer.serialize_tcp_hash(h)).to eq(ret[i])
1210
+ end
1211
+ end
1212
+ end
1213
+ end
1214
+
1215
+ # m:00,VgIEAQNCYWQK7WkCBEJ1cm8K8wADCldvaG56aW1tZXIK8wwEDFNjaGxhZnppbW1lcgr
1216
+ # 1QAUCCu1pS0VRMDM3ODA0MAZIVCBCYWQBAgrzAEtFUTAzNzk1NDQHSFQgQnVybwICCvMMS0VRMD
1217
+ # M3OTU1NhlIVCBXb2huemltbWVyIEJhbGtvbnNlaXRlAwIK83lLRVEwMzc5NjY1GkhUIFdvaG56a
1218
+ # W1tZXIgRmVuc3RlcnNlaXRlAwIK9UBLRVEwMzgwMTIwD0hUIFNjaGxhZnppbW1lcgQB
1219
+ describe 'm message' do
1220
+ context 'invalid hash' do
1221
+ let(:hashes) do
1222
+ [
1223
+ # invalid values
1224
+ {
1225
+ type: 'm', index: 'x', rooms_count: 1, devices_count: 1,
1226
+ rooms: [{ id: '!'.unpack1('C'), name_length: 2, name: 'XY', rf_address: 0x313233}],
1227
+ devices: [{ type: :shutter_contact, rf_address: 0x524641, serial_number: 'serial_num', name_length: 4, name: 'NAME', room_id: '!'.unpack1('C') }],
1228
+ },
1229
+ {
1230
+ type: 'm', rooms_count: 'x', devices_count: 1,
1231
+ rooms: [{ id: '!'.unpack1('C'), name_length: 2, name: 'XY', rf_address: 0x313233}],
1232
+ devices: [{ type: :shutter_contact, rf_address: 0x524641, serial_number: 'serial_num', name_length: 4, name: 'NAME', room_id: '!'.unpack1('C') }],
1233
+ },
1234
+ {
1235
+ type: 'm', rooms_count: '1', devices_count: 'x',
1236
+ rooms: [{ id: '!'.unpack1('C'), name_length: 2, name: 'XY', rf_address: 0x313233}],
1237
+ devices: [{ type: :shutter_contact, rf_address: 0x524641, serial_number: 'serial_num', name_length: 4, name: 'NAME', room_id: '!'.unpack1('C') }],
1238
+ },
1239
+ {
1240
+ type: 'm', rooms_count: '1', devices_count: '1',
1241
+ rooms: [{ id: '!', name_length: '2', name: 'XY', rf_address: 0x313233}],
1242
+ devices: [{ type: :shutter_contact, rf_address: 0x524641, serial_number: 'serial_num', name_length: 4, name: 'NAME', room_id: '!'.unpack1('C') }],
1243
+ },
1244
+ {
1245
+ type: 'm', rooms_count: '1', devices_count: '1',
1246
+ rooms: [{ id: '!'.unpack1('C'), name_length: 'x', name: 'XY', rf_address: 0x313233}],
1247
+ devices: [{ type: :shutter_contact, rf_address: 0x524641, serial_number: 'serial_num', name_length: 4, name: 'NAME', room_id: '!'.unpack1('C') }],
1248
+ },
1249
+ {
1250
+ type: 'm', rooms_count: '1', devices_count: '1',
1251
+ rooms: [{ id: '!'.unpack1('C'), name_length: '2', name: 'XY', rf_address: 'x313233'}],
1252
+ devices: [{ type: :shutter_contact, rf_address: 0x524641, serial_number: 'serial_num', name_length: 4, name: 'NAME', room_id: '!'.unpack1('C') }],
1253
+ },
1254
+ {
1255
+ type: 'm', rooms_count: '1', devices_count: '1',
1256
+ rooms: [{ id: '!'.unpack1('C'), name_length: '2', name: 'XY', rf_address: '0x313233'}],
1257
+ devices: [{ type: 'shutter_contact', rf_address: 'x524641', serial_number: 'serial_num', name_length: 4, name: 'NAME', room_id: '!'.unpack1('C') }],
1258
+ },
1259
+ {
1260
+ type: 'm', rooms_count: '1', devices_count: '1',
1261
+ rooms: [{ id: '!'.unpack1('C'), name_length: '2', name: 'XY', rf_address: '0x313233'}],
1262
+ devices: [{ type: 'shutter_contact', rf_address: '0x524641', serial_number: 'serial_num', name_length: 'x', name: 'NAME', room_id: '!'.unpack1('C') }],
1263
+ },
1264
+ {
1265
+ type: 'm', rooms_count: '1', devices_count: '1',
1266
+ rooms: [{ id: '!'.unpack1('C'), name_length: '2', name: 'XY', rf_address: '0x313233'}],
1267
+ devices: [{ type: 'shutter_contact', rf_address: '0x524641', serial_number: 'serial_num', name_length: 4, name: 'NAME', room_id: '!' }],
1268
+ },
1269
+
1270
+ # name length mismatch
1271
+ {
1272
+ type: 'm', rooms_count: '1', devices_count: '1',
1273
+ rooms: [{ id: '!'.unpack1('C'), name_length: '3', name: 'XY', rf_address: '0x313233'}],
1274
+ devices: [{ type: :shutter_contact, rf_address: 0x524641, serial_number: 'serial_num', name_length: 4, name: 'NAME', room_id: '!'.unpack1('C') }],
1275
+ },
1276
+ {
1277
+ type: 'm', rooms_count: '1', devices_count: '1',
1278
+ rooms: [{ id: '!'.unpack1('C'), name_length: 2, name: 'XY', rf_address: '0x313233'}],
1279
+ devices: [{ type: :shutter_contact, rf_address: 0x524641, serial_number: 'serial_num', name_length: 3, name: 'NAME', room_id: '!'.unpack1('C') }],
1280
+ },
1281
+ ]
1282
+ end
1283
+ it 'raises proper exception' do
1284
+ hashes.each do |h|
1285
+ expect{ serializer.serialize_tcp_hash(h) }.to raise_error Messages::InvalidMessageBody
1286
+ end
1287
+ end
1288
+ end
1289
+ context 'valid hash' do
1290
+ let(:hashes) do
1291
+ [
1292
+ {
1293
+ type: 'm', index: 0, unknown1: 'Vx', unknown2: "\x01".b,
1294
+ rooms_count: 1, rooms: [
1295
+ { id: '!'.unpack1('C'), name_length: 2, name: 'XY', rf_address: 0x313233},
1296
+ ],
1297
+ devices_count: 1, devices: [
1298
+ { type: 'shutter_contact', rf_address: 0x524641, serial_number: 'serial_num', name_length: 4, name: 'NAME', room_id: '!'.unpack1('C') },
1299
+ ],
1300
+ },
1301
+ {
1302
+ type: 'm',
1303
+ rooms_count: 1, rooms: [
1304
+ { id: '!'.unpack1('C'), name: 'XY', rf_address: 0x313233},
1305
+ ],
1306
+ devices_count: 1, devices: [
1307
+ { type: :shutter_contact, rf_address: 0x524641, serial_number: 'serial_num', name: 'NAME', room_id: '!'.unpack1('C') },
1308
+ ],
1309
+ },
1310
+ {
1311
+ type: 'm', index: 0, unknown1: "V\x02".b, unknown2: "\x01".b,
1312
+ rooms_count: 4, rooms: [
1313
+ { id: 1, name_length: 3, name: 'Bad', rf_address: 0x0aed69 },
1314
+ { id: 2, name_length: 4, name: 'Buro', rf_address: 0x0af300 },
1315
+ { id: 3, name_length: 10, name: 'Wohnzimmer', rf_address: 0x0af30c },
1316
+ { id: 4, name_length: 12, name: 'Schlafzimmer', rf_address: 0x0af540 },
1317
+ ],
1318
+ devices_count: 5, devices: [
1319
+ { type: :radiator_thermostat_plus, rf_address: 0x0aed69, serial_number: 'KEQ0378040', name_length: 6, name: 'HT Bad', room_id: 1 },
1320
+ { type: :radiator_thermostat_plus, rf_address: 0x0af300, serial_number: 'KEQ0379544', name_length: 7, name: 'HT Buro', room_id: 2 },
1321
+ { type: :radiator_thermostat_plus, rf_address: 0x0af30c, serial_number: 'KEQ0379556', name_length: 25, name: 'HT Wohnzimmer Balkonseite', room_id: 3 },
1322
+ { type: :radiator_thermostat_plus, rf_address: 0x0af379, serial_number: 'KEQ0379665', name_length: 26, name: 'HT Wohnzimmer Fensterseite', room_id: 3 },
1323
+ { type: :radiator_thermostat_plus, rf_address: 0x0af540, serial_number: 'KEQ0380120', name_length: 15, name: 'HT Schlafzimmer', room_id: 4},
1324
+ ],
1325
+ },
1326
+ ]
1327
+ end
1328
+ let(:ret) do
1329
+ [
1330
+ 'm:00,' + Base64.strict_encode64("Vx\x01!\x02XY123\x01\x04RFAserial_num\x04NAME!\x01"),
1331
+ # 'm:00,' + Base64.strict_encode64("Vx\x01!\x02XY123\x01\x04RFAserial_num\x04NAME!\x01"),
1332
+ 'm:00,' + Base64.strict_encode64("\x00\x00\x01!\x02XY123\x01\x04RFAserial_num\x04NAME!\x00"),
1333
+ 'm:00,VgIEAQNCYWQK7WkCBEJ1cm8K8wADCldvaG56aW1tZXIK8wwEDFNjaGxhZnppbW1lcgr' \
1334
+ '1QAUCCu1pS0VRMDM3ODA0MAZIVCBCYWQBAgrzAEtFUTAzNzk1NDQHSFQgQnVybwICCvMMS0VRMD' \
1335
+ 'M3OTU1NhlIVCBXb2huemltbWVyIEJhbGtvbnNlaXRlAwIK83lLRVEwMzc5NjY1GkhUIFdvaG56a' \
1336
+ 'W1tZXIgRmVuc3RlcnNlaXRlAwIK9UBLRVEwMzgwMTIwD0hUIFNjaGxhZnppbW1lcgQB',
1337
+ ].map { |s| s << "\r\n" }
1338
+ end
1339
+ it 'returns proper string' do
1340
+ hashes.each_with_index do |h, i|
1341
+ expect(serializer.serialize_tcp_hash(h)).to eq(ret[i])
1342
+ end
1343
+ end
1344
+ end
1345
+ end
1346
+
1347
+ # n:
1348
+ # n:003c
1349
+ describe 'n message' do
1350
+ context 'invalid hash' do
1351
+ let(:hashes) do
1352
+ [
1353
+ # invalid values
1354
+ { type: 'n', timeout: 'x' },
1355
+ ]
1356
+ end
1357
+ it 'raises proper exception' do
1358
+ hashes.each do |h|
1359
+ expect{ serializer.serialize_tcp_hash(h) }.to raise_error Messages::InvalidMessageBody
1360
+ end
1361
+ end
1362
+ end
1363
+ context 'valid hash' do
1364
+ let(:hashes) do
1365
+ [
1366
+ { type: 'n', },
1367
+ { type: 'n', timeout: 60 },
1368
+ ]
1369
+ end
1370
+ let(:ret) do
1371
+ [
1372
+ 'n:',
1373
+ 'n:003c',
1374
+ ].map { |s| s << "\r\n" }
1375
+ end
1376
+ it 'returns proper string' do
1377
+ hashes.each_with_index do |h, i|
1378
+ expect(serializer.serialize_tcp_hash(h)).to eq(ret[i])
1379
+ end
1380
+ end
1381
+ end
1382
+ end
1383
+
1384
+ # s:AARAAAAAB5EAAWY=
1385
+ describe 's message' do
1386
+ context 'invalid hash head' do
1387
+ let(:hashes) do
1388
+ [
1389
+ # invalid values
1390
+ { type: 's', unknown: "\x00".b,
1391
+ command: :set_temperature_mode,
1392
+ rf_address_from: 'x', rf_address: '1', room_id: 1,
1393
+ temperature: 19.0, mode: :manual, },
1394
+ { type: 's', unknown: "\x00".b,
1395
+ command: :set_temperature_mode,
1396
+ rf_address_from: '0', rf_address: 'x', room_id: 1,
1397
+ temperature: 19.0, mode: :manual, },
1398
+ { type: 's', unknown: "\x00".b,
1399
+ command: :set_temperature_modex,
1400
+ rf_address_from: '0', rf_address: '1', room_id: 1,
1401
+ temperature: 19.0, mode: :manual, },
1402
+ { type: 's', rf_flags: 'x1',
1403
+ command: :set_temperature_mode,
1404
+ rf_address_to: 0x179101, room_id: 3,
1405
+ temperature: 24.0, mode: :vacation, },
1406
+ ]
1407
+ end
1408
+ it 'raises proper exception' do
1409
+ hashes.each do |h|
1410
+ expect{ serializer.serialize_tcp_hash(h) }.to raise_error Messages::InvalidMessageBody
1411
+ end
1412
+ end
1413
+ end
1414
+ context 'set temperature and mode' do
1415
+ context 'invalid hash body' do
1416
+ let(:hashes) do
1417
+ [
1418
+ # invalid values
1419
+ { type: 's', unknown: "\x00".b,
1420
+ command: 'set_temperature_mode',
1421
+ rf_address_range: 0..0x079100, room_id: 'x',
1422
+ temperature: 19.0, mode: 'manual', },
1423
+ { type: 's', unknown: "\x00".b,
1424
+ command: 'set_temperature_mode',
1425
+ rf_address_range: 0..0x079100, room_id: '1',
1426
+ temperature: 'x', mode: 'manual', },
1427
+ { type: 's', unknown: "\x00".b,
1428
+ command: :set_temperature_mode,
1429
+ rf_address_range: 4..0x079100, room_id: 1,
1430
+ temperature: 19.0, mode: :vacation,
1431
+ datetime_until: 'x' },
1432
+ ]
1433
+ end
1434
+ it 'raises proper exception' do
1435
+ hashes.each do |h|
1436
+ expect{ serializer.serialize_tcp_hash(h) }.to raise_error Messages::InvalidMessageBody
1437
+ end
1438
+ end
1439
+ end
1440
+ context 'valid hash' do
1441
+ let(:hashes) do
1442
+ [
1443
+ { type: 's', unknown: "\x00".b,
1444
+ command: 'set_temperature_mode',
1445
+ rf_address_range: 0..0x079100, room_id: 1,
1446
+ temperature: 19.0, mode: 'manual', },
1447
+ { type: 's', unknown: "\x00".b,
1448
+ command: :set_temperature_mode,
1449
+ rf_address_range: 4..0x079100, room_id: 1,
1450
+ temperature: 19.0, mode: :vacation,
1451
+ datetime_until: Time.new(2011, 8, 29, 2, 0) },
1452
+ { type: 's', unknown: "\x00".b,
1453
+ command: :set_temperature_mode,
1454
+ rf_address_range: 4..0x079100, room_id: 1,
1455
+ temperature: 19.0, mode: :vacation,
1456
+ datetime_until: '2011-08-29 02:00' },
1457
+ { type: 's', unknown: "\x00".b,
1458
+ command: :set_temperature_mode,
1459
+ rf_address: 0x179101, room_id: 3,
1460
+ temperature: 19.0, mode: :vacation,
1461
+ datetime_until: Time.new(2015, 11, 29, 2, 0) },
1462
+ { type: 's', unknown: "\x00".b, rf_flags: 0x4,
1463
+ command: :set_temperature_mode,
1464
+ rf_address_to: 0x179101, room_id: 3,
1465
+ temperature: 24.0, mode: :vacation,
1466
+ datetime_until: Time.new(2016, 11, 29, 2, 30) },
1467
+ { type: 's', unknown: "\x00".b, rf_flags: 0x4,
1468
+ command: :set_temperature_mode,
1469
+ rf_address_from: 0x01, rf_address_to: 0x179101, room_id: 3,
1470
+ temperature: 24.5, mode: :vacation,
1471
+ datetime_until: Time.new(2016, 11, 29, 2, 29) },
1472
+ { type: 's', unknown: "\x00".b, rf_flags: 0x3,
1473
+ command: :set_temperature_mode,
1474
+ rf_address_from: 0x01, rf_address: 0x179101, room_id: 3,
1475
+ temperature: 24.5, mode: :vacation,
1476
+ datetime_until: Time.new(2016, 1, 29, 10, 59) },
1477
+ { type: 's', unknown: "\x00".b, rf_flags: 0x0,
1478
+ command: :set_temperature_mode,
1479
+ rf_address: 0x0fdaed, room_id: 1,
1480
+ temperature: 19.0, mode: :boost, },
1481
+ { type: 's', unknown: "\x00".b,
1482
+ command: :set_temperature_mode,
1483
+ rf_address: 0x122b65, room_id: 1,
1484
+ temperature: 19.0, mode: :manual, },
1485
+ { type: 's', unknown: "\x00".b,
1486
+ command: :set_temperature_mode,
1487
+ rf_address: 0x122b65, room_id: 1,
1488
+ temperature: 0.0, mode: :auto, },
1489
+ { type: 's', unknown: "\x00".b,
1490
+ command: :set_temperature_mode,
1491
+ rf_address: 0x122b65, room_id: 1,
1492
+ temperature: 29.5, mode: :vacation,
1493
+ datetime_until: Time.new(2015, 12, 15, 23, 00) },
1494
+ ]
1495
+ end
1496
+ let(:ret) do
1497
+ [
1498
+ 's:AARAAAAAB5EAAWY=',
1499
+ 's:' + Base64.strict_encode64("\x00\x04\x40\x00\x00\x04\x07\x91\x00\x01\xa6\x9d\x0b\x04"),
1500
+ 's:' + Base64.strict_encode64("\x00\x04\x40\x00\x00\x04\x07\x91\x00\x01\xa6\x9d\x0b\x04"),
1501
+ 's:' + Base64.strict_encode64("\x00\x04\x40\x00\x00\x00\x17\x91\x01\x03\xa6\xbd\x8f\x04"),
1502
+ 's:' + Base64.strict_encode64("\x00\x04\x40\x00\x00\x00\x17\x91\x01\x03\xb0\xbd\x90\x05"),
1503
+ 's:' + Base64.strict_encode64("\x00\x04\x40\x00\x00\x01\x17\x91\x01\x03\xb1\xbd\x90\x04"),
1504
+ 's:' + Base64.strict_encode64("\x00\x03\x40\x00\x00\x01\x17\x91\x01\x03\xb1\x1d\x90\x15"),
1505
+ 's:' + Base64.strict_encode64("\x00\x00\x40\x00\x00\x00\x0f\xda\xed\x01\xe6"),
1506
+ 's:' + Base64.strict_encode64("\x00\x04\x40\x00\x00\x00\x12\x2b\x65\x01\x66"),
1507
+ 's:' + Base64.strict_encode64("\x00\x04\x40\x00\x00\x00\x12\x2b\x65\x01\x00"),
1508
+ 's:' + Base64.strict_encode64("\x00\x04\x40\x00\x00\x00\x12\x2b\x65\x01\xbb\xcf\x0f\x2e"),
1509
+ ].map { |s| s << "\r\n" }
1510
+ end
1511
+ it 'returns proper string' do
1512
+ hashes.each_with_index do |h, i|
1513
+ expect(serializer.serialize_tcp_hash(h)).to eq(ret[i])
1514
+ end
1515
+ end
1516
+ end
1517
+ end
1518
+ context 'set program' do
1519
+ context 'invalid hash body' do
1520
+ let(:hashes) do
1521
+ [
1522
+ # invalid values
1523
+ { type: 's',
1524
+ command: :set_program,
1525
+ rf_address: 0x122b65, room_id: 'x',
1526
+ day: 'Tuesday', program: [],
1527
+ },
1528
+ { type: 's',
1529
+ command: :set_program,
1530
+ rf_address: 0x122b65, room_id: '1',
1531
+ day: 'TuesdayX', program: [],
1532
+ },
1533
+ { type: 's',
1534
+ command: :set_program,
1535
+ rf_address: 0x122b65, room_id: '1',
1536
+ day: '0', program: [],
1537
+ },
1538
+ { type: 's',
1539
+ command: :set_program,
1540
+ rf_address: 0x122b65, room_id: '1',
1541
+ day: '8', program: [],
1542
+ },
1543
+ { type: 's',
1544
+ command: :set_program,
1545
+ rf_address: 0x122b65, room_id: 1, day: 'Sunday',
1546
+ program: [{ temperature: 'x', hours_until: 6, minutes_until: 5, }],
1547
+ },
1548
+ { type: 's',
1549
+ command: :set_program,
1550
+ rf_address: 0x122b65, room_id: 1, day: 'Sunday',
1551
+ program: [{ temperature: 10, hours_until: 'x', minutes_until: 5, }],
1552
+ },
1553
+ { type: 's',
1554
+ command: :set_program,
1555
+ rf_address: 0x122b65, room_id: 1, day: 'Sunday',
1556
+ program: [{ temperature: 12, hours_until: 5, minutes_until: 'x', }],
1557
+ },
1558
+ ]
1559
+ end
1560
+ it 'raises proper exception' do
1561
+ hashes.each do |h|
1562
+ expect{ serializer.serialize_tcp_hash(h) }.to raise_error Messages::InvalidMessageBody
1563
+ end
1564
+ end
1565
+ end
1566
+ context 'valid hash' do
1567
+ let(:hashes) do
1568
+ [
1569
+ { type: 's', unknown: "\x00".b,
1570
+ command: :set_program,
1571
+ rf_address: 0x0fc380, room_id: 1, day: 'monday',
1572
+ program: [
1573
+ { temperature: '16.0', hours_until: 6, minutes_until: 5, },
1574
+ { temperature: '19', hours_until: '9', minutes_until: '10', },
1575
+ { temperature: 16, hours_until: '16', minutes_until: 55, },
1576
+ { temperature: 19.0, hours_until: 24, minutes_until: '0', },
1577
+ { temperature: 19.0, hours_until: 24, minutes_until: 0, },
1578
+ { temperature: 19.0, hours_until: 24, minutes_until: 0, },
1579
+ { temperature: 19.0, hours_until: 24, minutes_until: 0, },
1580
+ ],
1581
+ },
1582
+ { type: 's',
1583
+ command: :set_program,
1584
+ rf_address: 0x122b65, room_id: 1, day: 'Tuesday',
1585
+ program: [],
1586
+ },
1587
+ { type: 's',
1588
+ command: :set_program,
1589
+ rf_address: 0x122b65, room_id: 1, day: '2',
1590
+ program: [],
1591
+ },
1592
+ { type: 's', unknown: "\x00".b,
1593
+ command: :set_program,
1594
+ rf_address: 0x122b65, room_id: 1, day: 'Sunday',
1595
+ program: [{ temperature: 16.0, hours_until: 6, minutes_until: 5, }],
1596
+ },
1597
+ ]
1598
+ end
1599
+ let(:ret) do
1600
+ [
1601
+ 's:AAQQAAAAD8OAAQJASUxuQMtNIE0gTSBNIA==',
1602
+ 's:' + Base64.strict_encode64("\x00\x04\x10\x00\x00\x00\x12\x2b\x65\x01\x03"),
1603
+ 's:' + Base64.strict_encode64("\x00\x04\x10\x00\x00\x00\x12\x2b\x65\x01\x03"),
1604
+ 's:' + Base64.strict_encode64("\x00\x04\x10\x00\x00\x00\x12\x2b\x65\x01\x01\x40\x49"),
1605
+ ].map { |s| s << "\r\n" }
1606
+ end
1607
+ it 'returns proper string' do
1608
+ hashes.each_with_index do |h, i|
1609
+ expect(serializer.serialize_tcp_hash(h)).to eq(ret[i])
1610
+ end
1611
+ end
1612
+ end
1613
+ end
1614
+ context 'set temperature' do
1615
+ context 'invalid hash body' do
1616
+ let(:hashes) do
1617
+ [
1618
+ # invalid values
1619
+ { type: 's',
1620
+ command: :set_temperature,
1621
+ rf_address: 0x0fc380, room_id: 'x',
1622
+ comfort_temperature: 21.5,
1623
+ eco_temperature: 16.5,
1624
+ max_setpoint_temperature: 30.5,
1625
+ min_setpoint_temperature: 4.5,
1626
+ temperature_offset: 0.0,
1627
+ window_open_temperature: 12.0,
1628
+ window_open_duration: 15,
1629
+ },
1630
+ { type: 's',
1631
+ command: :set_temperature,
1632
+ rf_address: 0x0fc380, room_id: '0',
1633
+ comfort_temperature: 'x',
1634
+ eco_temperature: 16.5,
1635
+ max_setpoint_temperature: 30.5,
1636
+ min_setpoint_temperature: 4.5,
1637
+ temperature_offset: 0.0,
1638
+ window_open_temperature: 12.0,
1639
+ window_open_duration: 15,
1640
+ },
1641
+ { type: 's',
1642
+ command: :set_temperature,
1643
+ rf_address: 0x0fc380, room_id: 0,
1644
+ comfort_temperature: 21.5,
1645
+ eco_temperature: 16.5,
1646
+ max_setpoint_temperature: 30.5,
1647
+ min_setpoint_temperature: 4.5,
1648
+ temperature_offset: '0..0',
1649
+ window_open_temperature: 12.0,
1650
+ window_open_duration: 15,
1651
+ },
1652
+ { type: 's',
1653
+ command: :set_temperature,
1654
+ rf_address: 0x0fc380, room_id: 0,
1655
+ comfort_temperature: 21.5,
1656
+ eco_temperature: 16.5,
1657
+ max_setpoint_temperature: 30.5,
1658
+ min_setpoint_temperature: 4.5,
1659
+ temperature_offset: 0.0,
1660
+ window_open_temperature: 12.0,
1661
+ window_open_duration: 'x',
1662
+ },
1663
+ ]
1664
+ end
1665
+ it 'raises proper exception' do
1666
+ hashes.each do |h|
1667
+ expect{ serializer.serialize_tcp_hash(h) }.to raise_error Messages::InvalidMessageBody
1668
+ end
1669
+ end
1670
+ end
1671
+ context 'valid hash' do
1672
+ let(:hashes) do
1673
+ [
1674
+ { type: 's', unknown: "\x00".b,
1675
+ command: :set_temperature,
1676
+ rf_address: 0x0fc380, room_id: 0,
1677
+ comfort_temperature: 21.5,
1678
+ eco_temperature: 16.5,
1679
+ max_setpoint_temperature: 30.5,
1680
+ min_setpoint_temperature: 4.5,
1681
+ temperature_offset: '0',
1682
+ window_open_temperature: '12.0',
1683
+ window_open_duration: '15',
1684
+ },
1685
+ { type: 's', unknown: "\x00".b,
1686
+ command: :set_temperature,
1687
+ rf_address: 0x122b65, room_id: '1',
1688
+ comfort_temperature: 1.0,
1689
+ eco_temperature: 2.0,
1690
+ max_setpoint_temperature: '4.0',
1691
+ min_setpoint_temperature: 3.0,
1692
+ temperature_offset: 5.0,
1693
+ window_open_temperature: 6.0,
1694
+ window_open_duration: 10,
1695
+ },
1696
+ ]
1697
+ end
1698
+ let(:ret) do
1699
+ [
1700
+ 's:AAARAAAAD8OAACshPQkHGAM=',
1701
+ 's:' + Base64.strict_encode64("\x00\x00\x11\x00\x00\x00\x12\x2b\x65\x01\x02\x04\x08\x06\x11\x0c\x02"),
1702
+ ].map { |s| s << "\r\n" }
1703
+ end
1704
+ it 'returns proper string' do
1705
+ hashes.each_with_index do |h, i|
1706
+ expect(serializer.serialize_tcp_hash(h)).to eq(ret[i])
1707
+ end
1708
+ end
1709
+ end
1710
+ end
1711
+ context 'config valve' do
1712
+ context 'invalid hash body' do
1713
+ let(:hashes) do
1714
+ [
1715
+ # invalid values
1716
+ { type: 's',
1717
+ command: :config_valve,
1718
+ rf_address: 0x0fc380, room_id: 'x',
1719
+ boost_duration: '5',
1720
+ valve_opening: '90.0',
1721
+ decalcification_day: '6',
1722
+ decalcification_hour: 12,
1723
+ max_valve_setting: 100.0,
1724
+ valve_offset: 0.0,
1725
+ },
1726
+ { type: 's',
1727
+ command: :config_valve,
1728
+ rf_address: 0x0fc380, room_id: '1',
1729
+ boost_duration: 'x',
1730
+ valve_opening: '90.0',
1731
+ decalcification_day: '6',
1732
+ decalcification_hour: 12,
1733
+ max_valve_setting: 100.0,
1734
+ valve_offset: 0.0,
1735
+ },
1736
+ { type: 's',
1737
+ command: :config_valve,
1738
+ rf_address: 0x0fc380, room_id: '1',
1739
+ boost_duration: '5',
1740
+ valve_opening: '90%',
1741
+ decalcification_day: '6',
1742
+ decalcification_hour: 12,
1743
+ max_valve_setting: 100.0,
1744
+ valve_offset: 0.0,
1745
+ },
1746
+ { type: 's',
1747
+ command: :config_valve,
1748
+ rf_address: 0x0fc380, room_id: '1',
1749
+ boost_duration: '5',
1750
+ valve_opening: '90.0',
1751
+ decalcification_day: 'x',
1752
+ decalcification_hour: 12,
1753
+ max_valve_setting: 100.0,
1754
+ valve_offset: 0.0,
1755
+ },
1756
+ { type: 's',
1757
+ command: :config_valve,
1758
+ rf_address: 0x0fc380, room_id: '1',
1759
+ boost_duration: '5',
1760
+ valve_opening: '90.0',
1761
+ decalcification_day: '1',
1762
+ decalcification_hour: 'x',
1763
+ max_valve_setting: 100.0,
1764
+ valve_offset: 0.0,
1765
+ },
1766
+ { type: 's',
1767
+ command: :config_valve,
1768
+ rf_address: 0x0fc380, room_id: '1',
1769
+ boost_duration: '5',
1770
+ valve_opening: '90.0',
1771
+ decalcification_day: '1',
1772
+ decalcification_hour: '1',
1773
+ max_valve_setting: '100%',
1774
+ valve_offset: 0.0,
1775
+ },
1776
+ { type: 's',
1777
+ command: :config_valve,
1778
+ rf_address: 0x0fc380, room_id: '1',
1779
+ boost_duration: '5',
1780
+ valve_opening: '90.0',
1781
+ decalcification_day: '1',
1782
+ decalcification_hour: '1',
1783
+ max_valve_setting: '100',
1784
+ valve_offset: '0%',
1785
+ },
1786
+ ]
1787
+ end
1788
+ it 'raises proper exception' do
1789
+ hashes.each do |h|
1790
+ expect{ serializer.serialize_tcp_hash(h) }.to raise_error Messages::InvalidMessageBody
1791
+ end
1792
+ end
1793
+ end
1794
+ context 'valid hash' do
1795
+ let(:hashes) do
1796
+ [
1797
+ { type: 's', unknown: "\x00".b,
1798
+ command: :config_valve,
1799
+ rf_address: 0x0fc380, room_id: 1,
1800
+ boost_duration: '5',
1801
+ valve_opening: '90.0',
1802
+ decalcification_day: '6',
1803
+ decalcification_hour: 12,
1804
+ max_valve_setting: '100',
1805
+ valve_offset: 0.0,
1806
+ },
1807
+ { type: 's', unknown: "\x00".b,
1808
+ command: :config_valve,
1809
+ rf_address: 0x122b65, room_id: 1,
1810
+ boost_duration: 10,
1811
+ valve_opening: 80.0,
1812
+ decalcification_day: 'Tuesday',
1813
+ decalcification_hour: 2,
1814
+ max_valve_setting: 100.0,
1815
+ valve_offset: '0',
1816
+ },
1817
+ ]
1818
+ end
1819
+ let(:ret) do
1820
+ [
1821
+ 's:AAQSAAAAD8OAATIM/wA=',
1822
+ 's:' + Base64.strict_encode64("\x00\x04\x12\x00\x00\x00\x12\x2b\x65\x01\x50\x62\xff\x00"),
1823
+ ].map { |s| s << "\r\n" }
1824
+ end
1825
+ it 'returns proper string' do
1826
+ hashes.each_with_index do |h, i|
1827
+ expect(serializer.serialize_tcp_hash(h)).to eq(ret[i])
1828
+ end
1829
+ end
1830
+ end
1831
+ end
1832
+ context 'add/remove link partner' do
1833
+ context 'invalid hash body' do
1834
+ let(:hashes) do
1835
+ [
1836
+ # invalid values
1837
+ { type: 's',
1838
+ command: :add_link_partner,
1839
+ rf_address: 0x0fc373, room_id: 'x',
1840
+ partner_rf_address: 0x0fdaed,
1841
+ partner_type: :radiator_thermostat,
1842
+ },
1843
+ { type: 's',
1844
+ command: :add_link_partner,
1845
+ rf_address: 0x0fc373, room_id: '1',
1846
+ partner_rf_address: 'x0fdaed',
1847
+ partner_type: :radiator_thermostat,
1848
+ },
1849
+ { type: 's',
1850
+ command: :add_link_partner,
1851
+ rf_address: 0x0fc373, room_id: '1',
1852
+ partner_rf_address: '0x0fdaed',
1853
+ partner_type: :radiator_thermostatx,
1854
+ },
1855
+ ]
1856
+ end
1857
+ it 'raises proper exception' do
1858
+ hashes.each do |h|
1859
+ expect{ serializer.serialize_tcp_hash(h) }.to raise_error Messages::InvalidMessageBody
1860
+ end
1861
+ end
1862
+ end
1863
+ context 'valid hash' do
1864
+ let(:hashes) do
1865
+ [
1866
+ { type: 's',
1867
+ command: :add_link_partner,
1868
+ rf_address: 0x0fc373, room_id: '0',
1869
+ partner_rf_address: '0x0fdaed',
1870
+ partner_type: 'radiator_thermostat',
1871
+ },
1872
+ { type: 's', unknown: "\x00".b,
1873
+ command: :remove_link_partner,
1874
+ rf_address: 0x0fc373, room_id: 0,
1875
+ partner_rf_address: 0x0fdaed,
1876
+ partner_type: :radiator_thermostat,
1877
+ },
1878
+ ]
1879
+ end
1880
+ let(:ret) do
1881
+ [
1882
+ 's:AAAgAAAAD8NzAA/a7QE=',
1883
+ 's:AAAhAAAAD8NzAA/a7QE=',
1884
+ ].map { |s| s << "\r\n" }
1885
+ end
1886
+ it 'returns proper string' do
1887
+ hashes.each_with_index do |h, i|
1888
+ expect(serializer.serialize_tcp_hash(h)).to eq(ret[i])
1889
+ end
1890
+ end
1891
+ end
1892
+ end
1893
+ context 'set/unset group address' do
1894
+ context 'invalid hash body' do
1895
+ let(:hashes) do
1896
+ [
1897
+ # invalid values
1898
+ { type: 's',
1899
+ command: :set_group_address,
1900
+ rf_address: 0x0fc380, room_id: 'x',
1901
+ },
1902
+ ]
1903
+ end
1904
+ it 'raises proper exception' do
1905
+ hashes.each do |h|
1906
+ expect{ serializer.serialize_tcp_hash(h) }.to raise_error Messages::InvalidMessageBody
1907
+ end
1908
+ end
1909
+ end
1910
+ context 'valid hash' do
1911
+ let(:hashes) do
1912
+ [
1913
+ { type: 's',
1914
+ command: :set_group_address,
1915
+ rf_address: 0x0fc380, room_id: '1',
1916
+ },
1917
+ { type: 's', unknown: "\x00".b,
1918
+ command: :unset_group_address,
1919
+ rf_address: 0x0fc380, room_id: 1,
1920
+ },
1921
+ ]
1922
+ end
1923
+ let(:ret) do
1924
+ [
1925
+ 's:AAAiAAAAD8OAAAE=',
1926
+ 's:AAAjAAAAD8OAAAE=',
1927
+ ].map { |s| s << "\r\n" }
1928
+ end
1929
+ it 'returns proper string' do
1930
+ hashes.each_with_index do |h, i|
1931
+ expect(serializer.serialize_tcp_hash(h)).to eq(ret[i])
1932
+ end
1933
+ end
1934
+ end
1935
+ end
1936
+ context 'setting of current temperature display' do
1937
+ context 'invalid hash body' do
1938
+ let(:hashes) do
1939
+ [
1940
+ # invalid values
1941
+ { type: 's',
1942
+ command: :display_temperature,
1943
+ rf_address: 0x123abc, room_id: 'x',
1944
+ display_temperature: :measured,
1945
+ },
1946
+ ]
1947
+ end
1948
+ it 'raises proper exception' do
1949
+ hashes.each do |h|
1950
+ expect{ serializer.serialize_tcp_hash(h) }.to raise_error Messages::InvalidMessageBody
1951
+ end
1952
+ end
1953
+ end
1954
+ context 'valid hash' do
1955
+ let(:hashes) do
1956
+ [
1957
+ { type: 's',
1958
+ command: :display_temperature,
1959
+ rf_address: 0x123abc, room_id: '0',
1960
+ display_temperature: :measured,
1961
+ },
1962
+ { type: 's', unknown: "\x00".b,
1963
+ command: :display_temperature,
1964
+ rf_address: 0x123abc, room_id: 0,
1965
+ display_temperature: 'configured',
1966
+ },
1967
+ { type: 's', unknown: "\x00".b,
1968
+ command: :display_temperature,
1969
+ rf_address: 0x123abc, room_id: 0,
1970
+ },
1971
+ { type: 's', unknown: "\x00".b,
1972
+ command: :display_temperature,
1973
+ rf_address: 0x123abc, room_id: 0,
1974
+ display_temperature: 'XXX',
1975
+ },
1976
+ ]
1977
+ end
1978
+ let(:ret) do
1979
+ [
1980
+ 's:' + Base64.strict_encode64("\x00\x00\x82\x00\x00\x00\x12\x3a\xbc\x00\x04"),
1981
+ 's:' + Base64.strict_encode64("\x00\x00\x82\x00\x00\x00\x12\x3a\xbc\x00\x00"),
1982
+ 's:' + Base64.strict_encode64("\x00\x00\x82\x00\x00\x00\x12\x3a\xbc\x00\x00"),
1983
+ 's:' + Base64.strict_encode64("\x00\x00\x82\x00\x00\x00\x12\x3a\xbc\x00\x00"),
1984
+ ].map { |s| s << "\r\n" }
1985
+ end
1986
+ it 'returns proper string' do
1987
+ hashes.each_with_index do |h, i|
1988
+ expect(serializer.serialize_tcp_hash(h)).to eq(ret[i])
1989
+ end
1990
+ end
1991
+ end
1992
+ end
1993
+ end
1994
+
1995
+ # t:01,1,Dx1U
1996
+ describe 't message' do
1997
+ context 'invalid hash' do
1998
+ let(:hashes) do
1999
+ [
2000
+ # invalid values
2001
+ { type: 't', count: 'x', force: 0, rf_addresses: [] },
2002
+ { type: 't', count: '1', force: 'x', rf_addresses: [] },
2003
+ { type: 't', count: '1', force: 'true', rf_addresses: ['x'] },
2004
+ # count mismatch
2005
+ { type: 't', count: 2, force: 0, rf_addresses: [1] },
2006
+ # no devices
2007
+ { type: 't', count: 0, force: 0, rf_addresses: [] },
2008
+ ]
2009
+ end
2010
+ it 'raises proper exception' do
2011
+ hashes.each do |h|
2012
+ expect{ serializer.serialize_tcp_hash(h) }.to raise_error Messages::InvalidMessageBody
2013
+ end
2014
+ end
2015
+ end
2016
+ context 'valid hash' do
2017
+ let(:hashes) do
2018
+ [
2019
+ { type: 't', count: 1, force: true, rf_addresses: [0x0f1d54], },
2020
+ { type: 't', count: 1, force: '0', rf_addresses: [0x0f1d54], },
2021
+ { type: 't', count: 1, force: 'false', rf_addresses: [0x0f1d54], },
2022
+ ]
2023
+ end
2024
+ let(:ret) do
2025
+ [
2026
+ 't:01,1,Dx1U',
2027
+ 't:01,0,Dx1U',
2028
+ 't:01,0,Dx1U',
2029
+ ].map { |s| s << "\r\n" }
2030
+ end
2031
+ it 'returns proper string' do
2032
+ hashes.each_with_index do |h, i|
2033
+ expect(serializer.serialize_tcp_hash(h)).to eq(ret[i])
2034
+ end
2035
+ end
2036
+ end
2037
+ end
2038
+
2039
+ # u:http://www.max-portal.elv.de:80
2040
+ describe 'u message' do
2041
+ context 'invalid hash' do
2042
+ let(:hashes) do
2043
+ [
2044
+ # invalid values
2045
+ { type: 'u', url: 'URL', port: 'x' },
2046
+ ]
2047
+ end
2048
+ it 'raises proper exception' do
2049
+ hashes.each do |h|
2050
+ expect{ serializer.serialize_tcp_hash(h) }.to raise_error Messages::InvalidMessageBody
2051
+ end
2052
+ end
2053
+ end
2054
+ context 'valid hash' do
2055
+ let(:hashes) do
2056
+ [
2057
+ { type: 'u', url: 'http://www.max-portal.elv.de', port: 80, },
2058
+ ]
2059
+ end
2060
+ let(:ret) do
2061
+ [
2062
+ 'u:http://www.max-portal.elv.de:80',
2063
+ ].map { |s| s << "\r\n" }
2064
+ end
2065
+ it 'returns proper string' do
2066
+ hashes.each_with_index do |h, i|
2067
+ expect(serializer.serialize_tcp_hash(h)).to eq(ret[i])
2068
+ end
2069
+ end
2070
+ end
2071
+ end
2072
+
2073
+ # z:1e,G,01
2074
+ describe 'z message' do
2075
+ context 'invalid hash' do
2076
+ let(:hashes) do
2077
+ [
2078
+ # invalid values
2079
+ { type: 'z', time: 'x', scope: 'all' },
2080
+ { type: 'z', time: 1, scope: 'all', id: 'x' },
2081
+ { type: 'z', time: 1, scope: 'allx' },
2082
+ ]
2083
+ end
2084
+ it 'raises proper exception' do
2085
+ hashes.each do |h|
2086
+ expect{ serializer.serialize_tcp_hash(h) }.to raise_error Messages::InvalidMessageBody
2087
+ end
2088
+ end
2089
+ end
2090
+ context 'valid hash' do
2091
+ let(:hashes) do
2092
+ [
2093
+ { type: 'z', time: 30, scope: :group, id: 1, },
2094
+ { type: 'z', time: 33, scope: :room, id: 2, },
2095
+ { type: 'z', time: 24, scope: :all, },
2096
+ { type: 'z', time: 1, scope: :device, },
2097
+ ]
2098
+ end
2099
+ let(:ret) do
2100
+ [
2101
+ 'z:1e,G,01',
2102
+ 'z:21,G,02',
2103
+ 'z:18,A',
2104
+ 'z:01,D',
2105
+ ].map { |s| s << "\r\n" }
2106
+ end
2107
+ it 'returns proper string' do
2108
+ hashes.each_with_index do |h, i|
2109
+ expect(serializer.serialize_tcp_hash(h)).to eq(ret[i])
2110
+ end
2111
+ end
2112
+ end
2113
+ end
2114
+
2115
+ end
2116
+
2117
+ end