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,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: f724f91f573430c052cc4111ded2e56f36b02349702e6f9dd9ed7e1929f7eee2
4
+ data.tar.gz: 8a474badc26be87026e855763bfa959b2b89a3165a2891d199ac67a3c26928e0
5
+ SHA512:
6
+ metadata.gz: d6e28eec5439846c020d105697f2263e060ebf5f638fbb7397f9d2d878d41452af13cdbd3cacd4caa71252a4ce4e4a364dc44eef2cfa07d72ace5c59abbb0b83
7
+ data.tar.gz: 5381d96a92770f4510da936f93cb88b0d5313833e771db10c0bca1d4fefd9b9ba284dcf455f8c7c4e0d468963aca004e40de651475d733318a281fc34fbda27c
@@ -0,0 +1,32 @@
1
+ AllCops:
2
+ TargetRubyVersion: 2.4
3
+
4
+ Metrics/ModuleLength:
5
+ Max: 200
6
+
7
+ Metrics/ClassLength:
8
+ Max: 200
9
+
10
+ Metrics/MethodLength:
11
+ Max: 30
12
+
13
+ Metrics/AbcSize:
14
+ Enabled: false
15
+
16
+ Style/TrailingCommaInLiteral:
17
+ Enabled: false
18
+
19
+ Style/TrailingCommaInArguments:
20
+ Enabled: false
21
+
22
+ Style/FrozenStringLiteralComment:
23
+ Enabled: false
24
+
25
+ Style/DoubleNegation:
26
+ Enabled: false
27
+
28
+ Style/RaiseArgs:
29
+ EnforcedStyle: compact
30
+
31
+ Documentation:
32
+ Enabled: false
data/Gemfile ADDED
@@ -0,0 +1,5 @@
1
+ source 'https://rubygems.org'
2
+
3
+ git_source(:github) {|repo_name| "https://github.com/#{repo_name}" }
4
+
5
+ gemspec
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2018 Tomaqa
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,35 @@
1
+ # eQ3/ELV Max! Cube TCP client
2
+
3
+ ## Installation
4
+
5
+ Add this line to your application's Gemfile:
6
+
7
+ ```ruby
8
+ gem 'maxcube-client'
9
+ ```
10
+
11
+ And then execute:
12
+
13
+ $ bundle
14
+
15
+ Or install it yourself as:
16
+
17
+ $ gem install maxcube-client
18
+
19
+ ## Usage
20
+
21
+ TODO: Write usage instructions here
22
+
23
+ ## Development
24
+
25
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
26
+
27
+ To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
28
+
29
+ ## Contributing
30
+
31
+ Bug reports and pull requests are welcome on GitHub at https://github.com/Tomaqa/maxcube-client.
32
+
33
+ ## License
34
+
35
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -0,0 +1,6 @@
1
+ require "bundler/gem_tasks"
2
+ require "rspec/core/rake_task"
3
+
4
+ RSpec::Core::RakeTask.new(:spec)
5
+
6
+ task :default => :spec
@@ -0,0 +1,8 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'bundler/setup'
4
+ require 'maxcube/network/tcp/client'
5
+ require 'maxcube/network/udp/client'
6
+ require 'irb'
7
+
8
+ IRB.start(__FILE__)
@@ -0,0 +1,31 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'bundler/setup'
4
+ require 'maxcube/network/tcp/client'
5
+ require 'maxcube/network/udp/client'
6
+
7
+ HELP_KEYS = %w[h -h help -help --help ? -?].freeze
8
+
9
+ help = ARGV.size == 1 && HELP_KEYS.include?(ARGV.first)
10
+ wrong_args = ARGV.size > 2
11
+
12
+ if help || wrong_args
13
+ puts "Wrong number of arguments: #{ARGV.size} (expected: 0..2)" if wrong_args
14
+ puts "USAGE: ruby #{__FILE__} [<help>|<host>] [<port>]\n" \
15
+ " <help> - on of these: #{HELP_KEYS}\n\n" \
16
+ "If no arguments are given, UDP discovery is performed.\n" \
17
+ 'Otherwise, TCP client is launched (unless help command entered).'
18
+ exit
19
+ end
20
+
21
+ if ARGV.empty?
22
+ puts "No arguments given - performing UDP discovery ...\n" \
23
+ "(For usage message, type one of these: #{HELP_KEYS})\n\n"
24
+ client = MaxCube::Network::UDP::Client.new
25
+ client.discovery
26
+ client.close
27
+ exit
28
+ end
29
+
30
+ client = MaxCube::Network::TCP::Client.new
31
+ client.connect(*ARGV)
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'bundler/setup'
4
+ require 'maxcube/network/tcp/sample_server'
5
+
6
+ unless ARGV.size <= 1
7
+ puts "Wrong number of arguments: #{ARGV.size} (expected: 0..1)"
8
+ puts "Usage: ruby #{__FILE__} [port]"
9
+ exit
10
+ end
11
+
12
+ server = MaxCube::Network::TCP::SampleServer.new(*ARGV)
13
+ server.run
@@ -0,0 +1,13 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ require 'bundler/setup'
4
+ require 'maxcube/network/udp/sample_socket'
5
+
6
+ unless ARGV.size <= 1
7
+ puts "Wrong number of arguments: #{ARGV.size} (expected: 0..1)"
8
+ puts "Usage: ruby #{__FILE__} [port]"
9
+ exit
10
+ end
11
+
12
+ socket = MaxCube::Network::UDP::SampleSocket.new(*ARGV)
13
+ socket.run
@@ -0,0 +1,6 @@
1
+ #!/usr/bin/env bash
2
+ set -euo pipefail
3
+ IFS=$'\n\t'
4
+ set -vx
5
+
6
+ bundle install
@@ -0,0 +1,6 @@
1
+ ---
2
+ :type: t
3
+ :count: 1
4
+ :force: false
5
+ :rf_addresses:
6
+ - 0x123456
@@ -0,0 +1,20 @@
1
+ ---
2
+ :type: m
3
+ :index: 0
4
+ :unknown1: Vx
5
+ :unknown2: "\x01"
6
+ :rooms_count: 1
7
+ :rooms:
8
+ - :id: 33
9
+ :name_length: 2
10
+ :name: XY
11
+ :rf_address: 3224115
12
+ :devices_count: 1
13
+ :devices:
14
+ - :type: shutter_contact
15
+ :rf_address: 5391937
16
+ :serial_number: serial_num
17
+ :name_length: 4
18
+ :name: NAME
19
+ :room_id: 33
20
+
@@ -0,0 +1,6 @@
1
+ ---
2
+ :type: f
3
+ :ntp_servers:
4
+ - server1
5
+ - server2
6
+
@@ -0,0 +1,13 @@
1
+ ---
2
+ :type: s
3
+ :unknown: "\0"
4
+ :command: :set_temperature
5
+ :rf_address: 1033088
6
+ :room_id: 0
7
+ :comfort_temperature: 21.5
8
+ :eco_temperature: 16.5
9
+ :max_setpoint_temperature: 30.5
10
+ :min_setpoint_temperature: 4.5
11
+ :temperature_offset: 0
12
+ :window_open_temperature: 12.0
13
+ :window_open_duration: 15
@@ -0,0 +1,12 @@
1
+ ---
2
+ :type: s
3
+ :unknown: "\0"
4
+ :command: :set_temperature_mode
5
+ :rf_address_range: !ruby/range
6
+ begin: 4
7
+ end: 495872
8
+ excl: false
9
+ :room_id: 1
10
+ :temperature: 19.0
11
+ :mode: :vacation
12
+ :datetime_until: !ruby/object:DateTime 2011-08-29 02:00:00.000000000 Z
@@ -0,0 +1,11 @@
1
+ ---
2
+ :type: s
3
+ :command: :config_valve
4
+ :rf_address: 1033088
5
+ :room_id: 1
6
+ :boost_duration: '5'
7
+ :valve_opening: '90.0'
8
+ :decalcification_day: '6'
9
+ :decalcification_hour: 12
10
+ :max_valve_setting: '100'
11
+ :valve_offset: 0.0
@@ -0,0 +1,4 @@
1
+ ---
2
+ :type: u
3
+ :url: url
4
+ :port: 80
@@ -0,0 +1,4 @@
1
+ ---
2
+ :type: z
3
+ :time: 5
4
+ :scope: room
@@ -0,0 +1,148 @@
1
+ require 'date'
2
+ require 'ipaddr'
3
+
4
+ module MaxCube
5
+ module Messages
6
+ DEVICE_MODE = %i[auto manual vacation boost].freeze
7
+ DEVICE_TYPE = %i[cube
8
+ radiator_thermostat radiator_thermostat_plus
9
+ wall_thermostat
10
+ shutter_contact eco_switch].freeze
11
+
12
+ DAYS_OF_WEEK = %w[Saturday Sunday Monday
13
+ Tuesday Wednesday Thursday Friday].freeze
14
+
15
+ PACK_FORMAT = %w[x C n N N].freeze
16
+
17
+ class InvalidMessage < RuntimeError; end
18
+
19
+ class InvalidMessageLength < InvalidMessage
20
+ def initialize(info = 'invalid message length')
21
+ super
22
+ end
23
+ end
24
+
25
+ class InvalidMessageType < InvalidMessage
26
+ def initialize(msg_type, info = 'invalid message type')
27
+ super("#{info}: #{msg_type}")
28
+ end
29
+ end
30
+
31
+ class InvalidMessageFormat < InvalidMessage
32
+ def initialize(info = 'invalid format')
33
+ super
34
+ end
35
+ end
36
+
37
+ class InvalidMessageBody < InvalidMessage
38
+ def initialize(msg_type, info = 'invalid format')
39
+ super("message type #{msg_type}: #{info}")
40
+ end
41
+ end
42
+
43
+ private
44
+
45
+ def conv_args(type, info, *args, &block)
46
+ info = info.to_s.tr('_', ' ')
47
+ args.map(&block)
48
+ rescue ArgumentError, TypeError
49
+ raise InvalidMessageBody
50
+ .new(@msg_type,
51
+ "invalid #{type} format of arguments #{args} (#{info})")
52
+ end
53
+
54
+ # Convert string of characters (not binary data!) to hex number
55
+ # For binary data use #String.unpack
56
+ def to_ints(base, info, *args)
57
+ base_str = base.zero? ? '' : "(#{base})"
58
+ conv_args("integer#{base_str}", info, *args) { |x| Integer(x, base) }
59
+ end
60
+
61
+ def to_int(base, info, arg)
62
+ to_ints(base, info, arg).first
63
+ end
64
+
65
+ def to_floats(info, *args)
66
+ conv_args('float', info, *args) { |x| Float(x) }
67
+ end
68
+
69
+ def to_float(info, arg)
70
+ to_floats(info, arg).first
71
+ end
72
+
73
+ def to_bools(info, *args)
74
+ conv_args('boolean', info, *args) do |arg|
75
+ if arg == !!arg
76
+ arg
77
+ elsif arg.nil?
78
+ false
79
+ elsif %w[true false].include?(arg)
80
+ arg == 'true'
81
+ else
82
+ !Integer(arg).zero?
83
+ end
84
+ end
85
+ end
86
+
87
+ def to_bool(info, arg)
88
+ to_bools(info, arg).first
89
+ end
90
+
91
+ def to_datetimes(info, *args)
92
+ conv_args('datetime', info, *args) do |arg|
93
+ if arg.is_a?(DateTime)
94
+ arg
95
+ elsif arg.respond_to?('to_datetime')
96
+ arg.to_datetime
97
+ else
98
+ DateTime.parse(arg)
99
+ end
100
+ end
101
+ end
102
+
103
+ def to_datetime(info, arg)
104
+ to_datetimes(info, arg).first
105
+ end
106
+
107
+ def ary_elem(ary, id, info)
108
+ elem = ary[id]
109
+ return elem if elem
110
+ raise InvalidMessageBody
111
+ .new(@msg_type, "unrecognized #{info} id: #{id}")
112
+ end
113
+
114
+ def ary_elem_id(ary, elem, info)
115
+ id = ary.index(elem)
116
+ return id if id
117
+ raise InvalidMessageBody
118
+ .new(@msg_type, "unrecognized #{info}: #{elem}")
119
+ end
120
+
121
+ def device_type(device_type_id)
122
+ ary_elem(DEVICE_TYPE, device_type_id, 'device type')
123
+ end
124
+
125
+ def device_type_id(device_type)
126
+ ary_elem_id(DEVICE_TYPE, device_type.to_sym, 'device type')
127
+ end
128
+
129
+ def device_mode(device_mode_id)
130
+ ary_elem(DEVICE_MODE, device_mode_id, 'device mode')
131
+ end
132
+
133
+ def device_mode_id(device_mode)
134
+ ary_elem_id(DEVICE_MODE, device_mode.to_sym, 'device mode')
135
+ end
136
+
137
+ def day_of_week(day_id)
138
+ ary_elem(DAYS_OF_WEEK, day_id, 'day of week')
139
+ end
140
+
141
+ def day_of_week_id(day)
142
+ if day.respond_to?('to_i') && day.to_i.between?(1, 7)
143
+ return (day.to_i + 1) % 7
144
+ end
145
+ ary_elem_id(DAYS_OF_WEEK, day.capitalize, 'day of week')
146
+ end
147
+ end
148
+ end
@@ -0,0 +1,154 @@
1
+ require 'base64'
2
+ require 'stringio'
3
+
4
+ require 'maxcube/messages'
5
+
6
+ module MaxCube
7
+ module Messages
8
+ module Handler
9
+ include Messages
10
+
11
+ def valid_data_type(raw_data)
12
+ raw_data.is_a?(String)
13
+ end
14
+
15
+ def check_data_type(raw_data)
16
+ raise TypeError unless valid_data_type(raw_data)
17
+ raw_data
18
+ end
19
+
20
+ def valid_msg_type(msg_type)
21
+ maybe_check_valid_msg_type(msg_type, false)
22
+ end
23
+
24
+ def check_msg_type(msg_type)
25
+ maybe_check_valid_msg_type(msg_type, true)
26
+ @msg_type
27
+ end
28
+
29
+ def valid_msg_msg_type(msg)
30
+ valid_msg_type(msg_msg_type(msg))
31
+ end
32
+
33
+ def check_msg_msg_type(msg)
34
+ check_msg_type(msg_msg_type(msg))
35
+ end
36
+
37
+ def valid_msg(msg)
38
+ valid_msg_msg_type(msg)
39
+ end
40
+
41
+ def check_msg(msg)
42
+ check_msg_msg_type(msg)
43
+ end
44
+
45
+ def valid_hash_msg_type(hash)
46
+ valid_msg_type(hash[:type])
47
+ end
48
+
49
+ def check_hash_msg_type(hash)
50
+ msg_type = hash[:type]
51
+ check_msg_type(msg_type)
52
+ msg_type
53
+ end
54
+
55
+ def msg_type_hash_keys(msg_type)
56
+ msg_type_which_hash_keys(msg_type, false)
57
+ end
58
+
59
+ def msg_type_hash_opt_keys(msg_type)
60
+ msg_type_which_hash_keys(msg_type, true)
61
+ end
62
+
63
+ def valid_hash_keys(hash)
64
+ maybe_check_valid_hash_keys(hash, false)
65
+ end
66
+
67
+ def check_hash_keys(hash)
68
+ maybe_check_valid_hash_keys(hash, true)
69
+ hash
70
+ end
71
+
72
+ def valid_hash_values(hash)
73
+ hash.none? { |_, v| v.nil? }
74
+ end
75
+
76
+ def check_hash_values(hash)
77
+ return hash if valid_hash_values(hash)
78
+ hash = hash.dup
79
+ hash.delete(:type)
80
+ raise InvalidMessageBody
81
+ .new(@msg_type, "invalid hash values: #{hash}")
82
+ end
83
+
84
+ def valid_hash(hash)
85
+ valid_hash_msg_type(hash) &&
86
+ valid_hash_keys(hash) &&
87
+ valid_hash_values(hash)
88
+ end
89
+
90
+ def check_hash(hash)
91
+ check_hash_msg_type(hash)
92
+ check_hash_keys(hash)
93
+ check_hash_values(hash)
94
+ hash
95
+ end
96
+
97
+ private
98
+
99
+ def msg_types
100
+ self.class.const_get('MSG_TYPES')
101
+ end
102
+
103
+ def maybe_check_valid_msg_type(msg_type, check)
104
+ valid = msg_type&.length == 1 &&
105
+ msg_types.include?(msg_type)
106
+ return valid ? msg_type : false unless check
107
+ @msg_type = msg_type
108
+ raise InvalidMessageType.new(@msg_type) unless valid
109
+ end
110
+
111
+ def valid_msg_part_lengths(lengths, *args)
112
+ return false if args.any?(&:nil?) ||
113
+ args.length < lengths.length
114
+ args.each_with_index.all? do |v, i|
115
+ !lengths[i] || v.length == lengths[i]
116
+ end
117
+ end
118
+
119
+ def check_msg_part_lengths(lengths, *args)
120
+ return if valid_msg_part_lengths(lengths, *args)
121
+ raise InvalidMessageBody
122
+ .new(@msg_type,
123
+ "invalid lengths of message parts #{args}" \
124
+ " (lengths should be: #{lengths})")
125
+ end
126
+
127
+ def msg_type_which_hash_keys(msg_type, optional = false)
128
+ str = "Message#{msg_type.upcase}::" + (optional ? 'OPT_KEYS' : 'KEYS')
129
+ self.class.const_defined?(str) ? self.class.const_get(str) : []
130
+ end
131
+
132
+ def maybe_check_valid_hash_keys(hash, check)
133
+ keys = msg_type_hash_keys(@msg_type).dup
134
+ opt_keys = msg_type_hash_opt_keys(@msg_type)
135
+
136
+ hash_keys = hash.keys - opt_keys - [:type]
137
+
138
+ valid = hash_keys.sort == keys.sort
139
+ return valid if !check || valid
140
+ raise InvalidMessageBody
141
+ .new(@msg_type, "invalid hash keys: #{hash_keys} " \
142
+ "(should be: #{keys})")
143
+ end
144
+
145
+ def encode(data)
146
+ Base64.strict_encode64(data)
147
+ end
148
+
149
+ def decode(data)
150
+ Base64.decode64(data)
151
+ end
152
+ end
153
+ end
154
+ end