mitsubishi_heatpump 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/bin/mitsubishi +146 -0
- data/lib/mitsubishi_heatpump/abc.rb +210 -0
- data/lib/mitsubishi_heatpump/api.rb +75 -0
- data/lib/mitsubishi_heatpump/data_types.rb +198 -0
- data/lib/mitsubishi_heatpump/protocol.rb +206 -0
- data/lib/mitsubishi_heatpump/util.rb +11 -0
- data/lib/mitsubishi_heatpump/version.rb +5 -0
- data/lib/mitsubishi_heatpump.rb +11 -0
- metadata +122 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 8314b54176fb56cef543e4d4f9d7fe66b80d1ac9ed10ad6da8ac847bb5afce70
|
|
4
|
+
data.tar.gz: f8a8874b0f8211523354c406a08e03e2fceeee7bac2642cf3a3ee9ed77e134c4
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: bbe752efd1571bba5eb2aa708ee4a43c521a4bdfc221952175591a1fb0c980c833224bf5f7d2d5f85f9c2d4fd2032141171122661640ceb7ca9c22932e7556c5
|
|
7
|
+
data.tar.gz: df5b374886f07d08e0cc7f17b9999c615581fbbe5ac39e289c233355f18bffb24f7e2dc6199807422e1bb39c3308e95e57074c558f266b754621bcbdfdb0db74
|
data/bin/mitsubishi
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "optparse"
|
|
5
|
+
require "mitsubishi_heatpump"
|
|
6
|
+
|
|
7
|
+
std_opts = { port: 80 }
|
|
8
|
+
settings = {}
|
|
9
|
+
|
|
10
|
+
# rubocop:todo Metrics/BlockLength -- Refactor into Parser class
|
|
11
|
+
option_parser = OptionParser.new do |opts|
|
|
12
|
+
opts.banner = "Usage: mitsubishi HOST [options]"
|
|
13
|
+
|
|
14
|
+
opts.on("HOST", "Heatpump host") do |host|
|
|
15
|
+
std_opts[:host] = host
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
opts.on("-p", "--port PORT", Integer, "Heatpump port (default 80)") do |port|
|
|
19
|
+
std_opts[:port] = port
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
opts.on("--power POWER", "Set power (on/off)") do |power|
|
|
23
|
+
if power.downcase == "on"
|
|
24
|
+
settings[:power] = MitsubishiHeatpump::DataTypes::Power.ON
|
|
25
|
+
elsif power.downcase == "off"
|
|
26
|
+
settings[:power] = MitsubishiHeatpump::DataTypes::Power.OFF
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
opts.on("--target-temp TEMP", Integer, "Set target temperature") do |temp|
|
|
31
|
+
settings[:target_temp] = MitsubishiHeatpump::DataTypes::EnhancedTemperature.new(temp)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
opts.on("--mode MODE", "Set mode (auto/cool/heat/dry/fan)") do |mode|
|
|
35
|
+
case mode.downcase
|
|
36
|
+
when "auto"
|
|
37
|
+
settings[:operation_mode] = MitsubishiHeatpump::DataTypes::OperationMode.AUTO
|
|
38
|
+
when "cool"
|
|
39
|
+
settings[:operation_mode] = MitsubishiHeatpump::DataTypes::OperationMode.COOL
|
|
40
|
+
when "heat"
|
|
41
|
+
settings[:operation_mode] = MitsubishiHeatpump::DataTypes::OperationMode.HEAT
|
|
42
|
+
when "dry"
|
|
43
|
+
settings[:operation_mode] = MitsubishiHeatpump::DataTypes::OperationMode.DEHUMIDIFY
|
|
44
|
+
when "fan"
|
|
45
|
+
settings[:operation_mode] = MitsubishiHeatpump::DataTypes::OperationMode.FAN
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
opts.on("--fan-speed SPEED", "Set fan speed (auto/quiet/low/medium/high/very_high)") do |speed|
|
|
50
|
+
settings[:fan_speed] = case speed.downcase
|
|
51
|
+
when "auto"
|
|
52
|
+
MitsubishiHeatpump::DataTypes::FanMode.AUTO
|
|
53
|
+
when "quiet"
|
|
54
|
+
MitsubishiHeatpump::DataTypes::FanMode.QUIET
|
|
55
|
+
when "low"
|
|
56
|
+
MitsubishiHeatpump::DataTypes::FanMode.LOW
|
|
57
|
+
when "medium"
|
|
58
|
+
MitsubishiHeatpump::DataTypes::FanMode.MEDIUM
|
|
59
|
+
when "high"
|
|
60
|
+
MitsubishiHeatpump::DataTypes::FanMode.HIGH
|
|
61
|
+
when "very_high"
|
|
62
|
+
MitsubishiHeatpump::DataTypes::FanMode.VERY_HIGH
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
opts.on("--vertical-vane POSITION", "Set vertical vane position (auto/1-5/swing)") do |position|
|
|
67
|
+
position = case position.downcase
|
|
68
|
+
when "auto"
|
|
69
|
+
0
|
|
70
|
+
when "swing"
|
|
71
|
+
7
|
|
72
|
+
else
|
|
73
|
+
position.to_i
|
|
74
|
+
end
|
|
75
|
+
settings[:vertical_vane] = MitsubishiHeatpump::DataTypes::VerticalVane.new(position)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
opts.on("--horizontal-vane POSITION", "Set horizontal vane position (auto/full_left/left/center/right/full_right/"\
|
|
79
|
+
"split_center_left/split_center_right/split_left_right/split_center_left_right/swing)") do |position|
|
|
80
|
+
settings[:horizontal_vane] = case position.downcase
|
|
81
|
+
when "auto"
|
|
82
|
+
MitsubishiHeatpump::DataTypes::HorizontalVane.AUTO
|
|
83
|
+
when "full_left"
|
|
84
|
+
MitsubishiHeatpump::DataTypes::HorizontalVane.FULL_LEFT
|
|
85
|
+
when "left"
|
|
86
|
+
MitsubishiHeatpump::DataTypes::HorizontalVane.LEFT
|
|
87
|
+
when "center"
|
|
88
|
+
MitsubishiHeatpump::DataTypes::HorizontalVane.CENTER
|
|
89
|
+
when "right"
|
|
90
|
+
MitsubishiHeatpump::DataTypes::HorizontalVane.RIGHT
|
|
91
|
+
when "full_right"
|
|
92
|
+
MitsubishiHeatpump::DataTypes::HorizontalVane.FULL_RIGHT
|
|
93
|
+
when "split_center_left"
|
|
94
|
+
MitsubishiHeatpump::DataTypes::HorizontalVane.SPLIT_CENTER_LEFT
|
|
95
|
+
when "split_center_right"
|
|
96
|
+
MitsubishiHeatpump::DataTypes::HorizontalVane.SPLIT_CENTER_RIGHT
|
|
97
|
+
when "split_left_right"
|
|
98
|
+
MitsubishiHeatpump::DataTypes::HorizontalVane.SPLIT_LEFT_RIGHT
|
|
99
|
+
when "split_center_left_right"
|
|
100
|
+
MitsubishiHeatpump::DataTypes::HorizontalVane.SPLIT_CENTER_LEFT_RIGHT
|
|
101
|
+
when "swing"
|
|
102
|
+
MitsubishiHeatpump::DataTypes::HorizontalVane.SWING
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
opts.on("--dry-run", "Perform a dry run without sending commands") do
|
|
107
|
+
std_opts[:dry_run] = true
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
opts.on_tail("-h", "--help", "Show this message") do
|
|
111
|
+
puts opts
|
|
112
|
+
exit
|
|
113
|
+
end
|
|
114
|
+
end
|
|
115
|
+
# rubocop:enable Metrics/BlockLength
|
|
116
|
+
|
|
117
|
+
option_parser.parse!(ARGV)
|
|
118
|
+
|
|
119
|
+
std_opts[:host] = ARGV.shift
|
|
120
|
+
|
|
121
|
+
if std_opts[:host].nil?
|
|
122
|
+
puts "Error: HOST is required\n"
|
|
123
|
+
puts option_parser.help
|
|
124
|
+
exit 1
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
api = MitsubishiHeatpump::API.new(std_opts[:host], std_opts[:port])
|
|
128
|
+
|
|
129
|
+
if settings.empty?
|
|
130
|
+
resp = api.status
|
|
131
|
+
else
|
|
132
|
+
command = MitsubishiHeatpump::Protocol::PDU.new(
|
|
133
|
+
MitsubishiHeatpump::Protocol::SetRequest.new(
|
|
134
|
+
MitsubishiHeatpump::Protocol::SetSettings.new(
|
|
135
|
+
**settings
|
|
136
|
+
)
|
|
137
|
+
)
|
|
138
|
+
)
|
|
139
|
+
puts "Sending command:"
|
|
140
|
+
puts command.inspect
|
|
141
|
+
resp = api.send_command(command) unless std_opts[:dry_run]
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
resp.each do |pdu|
|
|
145
|
+
puts "#{pdu}\n\n"
|
|
146
|
+
end
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MitsubishiHeatpump
|
|
4
|
+
module ABC
|
|
5
|
+
##
|
|
6
|
+
# Packet base class
|
|
7
|
+
class Packet
|
|
8
|
+
attr_reader :command
|
|
9
|
+
|
|
10
|
+
def initialize(command)
|
|
11
|
+
@command = command
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def serialize
|
|
15
|
+
[@command.class::COMMAND_ID] + @command.serialize
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def inspect
|
|
19
|
+
"#{self.class.name.split("::").last} => #{@command.inspect}"
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
alias to_s inspect
|
|
23
|
+
|
|
24
|
+
def self.parse(payload)
|
|
25
|
+
command_type = self::COMMAND_TYPES[payload[0]]
|
|
26
|
+
raise ProtocolError, "Unsupported command type" unless command_type
|
|
27
|
+
|
|
28
|
+
new(command_type.parse(payload[1..]))
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
##
|
|
33
|
+
# Command base class
|
|
34
|
+
class Command
|
|
35
|
+
UPDATE_FLAGS_SIZE = 0
|
|
36
|
+
def initialize(**kwargs)
|
|
37
|
+
kwargs.each do |key, value|
|
|
38
|
+
raise ArgumentError, "Unknown parameter: #{key}" unless self.class::BYTE_VALUES.key?(key)
|
|
39
|
+
|
|
40
|
+
instance_variable_set("@#{key}", value)
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def self.parse(payload)
|
|
45
|
+
new(**self::BYTE_VALUES.transform_values do |param|
|
|
46
|
+
param.cls.parse(
|
|
47
|
+
param.mask && param.byte.is_a?(Integer) ? payload[param.byte] & param.mask : payload[param.byte]
|
|
48
|
+
)
|
|
49
|
+
end)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# rubocop:disable Metrics/MethodLength, Metrics/AbcSize -- Hard to refactor without increasing runtime complexity
|
|
53
|
+
def serialize
|
|
54
|
+
update_flag_size = self.class::UPDATE_FLAGS_SIZE
|
|
55
|
+
update_flags = DataTypes::UpdateFlags.new(0, update_flag_size)
|
|
56
|
+
bytes = [0] * 15
|
|
57
|
+
self.class::BYTE_VALUES.each do |key, param|
|
|
58
|
+
byte = param.byte
|
|
59
|
+
flag = param.update_flag || 0
|
|
60
|
+
value = instance_variable_get("@#{key}")&.serialize
|
|
61
|
+
next unless value
|
|
62
|
+
|
|
63
|
+
update_flags |= flag
|
|
64
|
+
if byte.is_a?(Integer)
|
|
65
|
+
bytes[byte] |= value
|
|
66
|
+
else
|
|
67
|
+
bytes[byte] = value
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
update_flags.serialize + bytes[update_flag_size..]
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def inspect
|
|
74
|
+
"#{self.class.name.split("::").last} => " + instance_variables.map do |var|
|
|
75
|
+
"#{var[1..]}=#{instance_variable_get(var).inspect}"
|
|
76
|
+
end.join(", ")
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
alias to_s inspect
|
|
80
|
+
end
|
|
81
|
+
# rubocop:enable Metrics/MethodLength, Metrics/AbcSize
|
|
82
|
+
|
|
83
|
+
##
|
|
84
|
+
# Generic data type base class.
|
|
85
|
+
class DataType
|
|
86
|
+
attr_reader :value
|
|
87
|
+
|
|
88
|
+
def initialize(value, byte_size = 1)
|
|
89
|
+
raise ArgumentError, "Invalid #{self.class}: #{value}" unless validate?(value)
|
|
90
|
+
|
|
91
|
+
@value = value
|
|
92
|
+
@byte_size = byte_size
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def self.parse(data)
|
|
96
|
+
if data.is_a?(Array)
|
|
97
|
+
byte_size = data.length
|
|
98
|
+
value = 0
|
|
99
|
+
data.each_with_index do |b, i|
|
|
100
|
+
value |= b << (8 * (byte_size - 1 - i))
|
|
101
|
+
end
|
|
102
|
+
new(value, byte_size)
|
|
103
|
+
else
|
|
104
|
+
new(data)
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def serialize
|
|
109
|
+
if @byte_size == 1
|
|
110
|
+
@value
|
|
111
|
+
else
|
|
112
|
+
result = []
|
|
113
|
+
@byte_size.times do |i|
|
|
114
|
+
result << ((@value >> (8 * (@byte_size - 1 - i))) & 0xff)
|
|
115
|
+
end
|
|
116
|
+
result
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def inspect
|
|
121
|
+
"#{self.class.name.split("::").last} (#{@value})"
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
alias to_s inspect
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
##
|
|
128
|
+
# Range data type base class.
|
|
129
|
+
class RangeType < DataType
|
|
130
|
+
def validate?(data)
|
|
131
|
+
self.class::RANGE.include?(data)
|
|
132
|
+
end
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
##
|
|
136
|
+
# Enum data type base class.
|
|
137
|
+
class EnumType < DataType
|
|
138
|
+
def self.respond_to_missing?(method_name, include_private = false)
|
|
139
|
+
constants.include?(method_name) || super
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def self.method_missing(method_name, *args, &)
|
|
143
|
+
return new(const_get(method_name)) if respond_to_missing?(method_name)
|
|
144
|
+
|
|
145
|
+
super
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def validate?(data)
|
|
149
|
+
self.class.constants.map { |const| self.class.const_get(const) }.include?(data)
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def inspect
|
|
153
|
+
const_name = self.class.constants.find { |const| self.class.const_get(const) == @value }
|
|
154
|
+
"#{self.class.name.split("::").last}.#{const_name}"
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
##
|
|
159
|
+
# BitMap data type base class.
|
|
160
|
+
class BitMapType < EnumType
|
|
161
|
+
def respond_to_missing?(method_name, include_private = false)
|
|
162
|
+
valid = self.class.constants
|
|
163
|
+
valid |= valid.map { |c| "#{c.downcase}?".to_sym }
|
|
164
|
+
valid.include?(method_name) ||
|
|
165
|
+
super
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
def method_missing(method_name, *args, &)
|
|
169
|
+
if respond_to_missing?(method_name)
|
|
170
|
+
return (@value & self.class.const_get(method_name[..-2])).anybits? if method_name.to_s.end_with?("?")
|
|
171
|
+
|
|
172
|
+
return self | self.class.const_get(method_name)
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
super
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def |(other)
|
|
179
|
+
case other
|
|
180
|
+
when BitMapType
|
|
181
|
+
self.class.new(@value | other.value, @byte_size)
|
|
182
|
+
when Integer
|
|
183
|
+
self.class.new(@value | other, @byte_size)
|
|
184
|
+
else
|
|
185
|
+
raise ArgumentError, "Cannot perform bitwise OR with #{other.class}"
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def &(other)
|
|
190
|
+
case other
|
|
191
|
+
when BitMapType
|
|
192
|
+
self.class.new(@value & other.value, @byte_size)
|
|
193
|
+
when Integer
|
|
194
|
+
self.class.new(@value & other, @byte_size)
|
|
195
|
+
else
|
|
196
|
+
raise ArgumentError, "Cannot perform bitwise AND with #{other.class}"
|
|
197
|
+
end
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
def validate?(data)
|
|
201
|
+
data.is_a?(Integer) && data >= 0 && data <= self.class.constants.map { |const| self.class.const_get(const) }.sum
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
def inspect
|
|
205
|
+
flags = self.class.constants.select { |const| @value.anybits?(self.class.const_get(const)) }
|
|
206
|
+
"#{self.class.name.split("::").last} (#{flags.empty? ? value : flags.map(&:to_s).join("|")})"
|
|
207
|
+
end
|
|
208
|
+
end
|
|
209
|
+
end
|
|
210
|
+
end
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MitsubishiHeatpump
|
|
4
|
+
##
|
|
5
|
+
# Mitsubishi Heatpump API class for communicating with the heat pump unit.
|
|
6
|
+
class API
|
|
7
|
+
KEY_SIZE = 16
|
|
8
|
+
IV_SIZE = 16
|
|
9
|
+
DEFAULT_KEY = "unregistered\0\0\0\0"
|
|
10
|
+
|
|
11
|
+
def initialize(host, port = 80, key = DEFAULT_KEY)
|
|
12
|
+
raise ArgumentError, "Host is required" if host.nil?
|
|
13
|
+
|
|
14
|
+
@host = host
|
|
15
|
+
@port = port
|
|
16
|
+
@key = key.length < KEY_SIZE ? key + ("\x00" * (KEY_SIZE - key.length)) : key[0, KEY_SIZE]
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def encrypt_payload(payload)
|
|
20
|
+
cipher = OpenSSL::Cipher.new("AES-128-CBC")
|
|
21
|
+
cipher.encrypt
|
|
22
|
+
cipher.key = @key
|
|
23
|
+
cipher.padding = 0
|
|
24
|
+
iv = cipher.random_iv
|
|
25
|
+
payload += "\x80#{"\x00" * (16 - ((payload.length + 1) % 16))}"
|
|
26
|
+
encrypted = cipher.update(payload) + cipher.final
|
|
27
|
+
Base64.strict_encode64(iv + encrypted)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def decrypt_payload(encrypted_payload)
|
|
31
|
+
decoded = Base64.strict_decode64(encrypted_payload)
|
|
32
|
+
iv = decoded[0, IV_SIZE]
|
|
33
|
+
cipher = OpenSSL::Cipher.new("AES-128-CBC")
|
|
34
|
+
cipher.decrypt
|
|
35
|
+
cipher.key = @key
|
|
36
|
+
cipher.iv = iv
|
|
37
|
+
cipher.padding = 0
|
|
38
|
+
decrypted = cipher.update(decoded[IV_SIZE..]) + cipher.final
|
|
39
|
+
decrypted.encode.rstrip.encode("UTF-8", invalid: :replace, undef: :replace).chomp("\ufffd")
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def make_request(payload)
|
|
43
|
+
body = "<?xml version=\"1.0\" encoding=\"UTF-8\"?><ESV>#{encrypt_payload(payload)}</ESV>"
|
|
44
|
+
Net::HTTP.start(@host, @port, use_ssl: false) do |http|
|
|
45
|
+
req = Net::HTTP::Post.new("/smart")
|
|
46
|
+
req.body = body
|
|
47
|
+
res = http.request(req)
|
|
48
|
+
xml = Nokogiri::XML.parse(res.body)
|
|
49
|
+
return parse_response(decrypt_payload(xml.at("ESV").content))
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def status
|
|
54
|
+
payload = "<CSV><CONNECT>ON</CONNECT></CSV>"
|
|
55
|
+
make_request(payload)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def send_command(command)
|
|
59
|
+
payload = command.serialize
|
|
60
|
+
make_request("<CSV><CONNECT>ON</CONNECT><CODE><VALUE>#{payload}</VALUE></CODE></CSV>")
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def parse_response(resp)
|
|
64
|
+
pdus = []
|
|
65
|
+
xml = Nokogiri::XML.parse(resp)
|
|
66
|
+
|
|
67
|
+
xml.xpath("./LSV/CODE/VALUE").each do |node|
|
|
68
|
+
pdus << MitsubishiHeatpump::Protocol::PDU.parse(node.content)
|
|
69
|
+
rescue MitsubishiHeatpump::ProtocolError
|
|
70
|
+
next
|
|
71
|
+
end
|
|
72
|
+
pdus
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
end
|
|
@@ -0,0 +1,198 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MitsubishiHeatpump
|
|
4
|
+
module DataTypes
|
|
5
|
+
class Power < ABC::EnumType
|
|
6
|
+
OFF = 0
|
|
7
|
+
ON = 1
|
|
8
|
+
TEST_MODE = 2
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
class OperatingMode < ABC::EnumType
|
|
12
|
+
HEAT = 1
|
|
13
|
+
DEHUMIDIFY = 2
|
|
14
|
+
COOL = 3
|
|
15
|
+
FAN = 7
|
|
16
|
+
AUTO = 8
|
|
17
|
+
I_SEE_HEAT = 9
|
|
18
|
+
I_SEE_DRY = 10
|
|
19
|
+
I_SEE_COOL = 11
|
|
20
|
+
AUTO_HEAT = 33
|
|
21
|
+
AUTO_COOL = 35
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
class FanMode < ABC::EnumType
|
|
25
|
+
AUTO = 0
|
|
26
|
+
QUIET = 1
|
|
27
|
+
LOW = 2
|
|
28
|
+
MEDIUM = 3
|
|
29
|
+
HIGH = 5
|
|
30
|
+
VERY_HIGH = 6
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
class VerticalVane < ABC::EnumType
|
|
34
|
+
AUTO = 0
|
|
35
|
+
POS_1 = 1
|
|
36
|
+
POS_2 = 2
|
|
37
|
+
POS_3 = 3
|
|
38
|
+
POS_4 = 4
|
|
39
|
+
POS_5 = 5
|
|
40
|
+
SWING = 7
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
##
|
|
44
|
+
# Horizontal vane position data type.
|
|
45
|
+
class HorizontalVane < ABC::EnumType
|
|
46
|
+
AUTO = 0
|
|
47
|
+
FULL_LEFT = 1
|
|
48
|
+
LEFT = 2
|
|
49
|
+
CENTER = 3
|
|
50
|
+
RIGHT = 4
|
|
51
|
+
FULL_RIGHT = 5
|
|
52
|
+
SPLIT_CENTER_LEFT = 6
|
|
53
|
+
SPLIT_CENTER_RIGHT = 7
|
|
54
|
+
SPLIT_LEFT_RIGHT = 8
|
|
55
|
+
SPLIT_CENTER_LEFT_RIGHT = 9
|
|
56
|
+
SWING = 12
|
|
57
|
+
|
|
58
|
+
def self.parse(data)
|
|
59
|
+
super(data & 0x0f)
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
##
|
|
64
|
+
# Horizontal vane adjustment data type.
|
|
65
|
+
class HorizontalVaneAdjustment < ABC::EnumType
|
|
66
|
+
OFF = 0
|
|
67
|
+
ON = 128
|
|
68
|
+
|
|
69
|
+
def self.parse(data)
|
|
70
|
+
super(data & 0xf0)
|
|
71
|
+
end
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
class TimerMode < ABC::EnumType
|
|
75
|
+
NONE = 0
|
|
76
|
+
OFF = 1
|
|
77
|
+
ON = 2
|
|
78
|
+
BOTH = 3
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
class FilterReset < ABC::EnumType
|
|
82
|
+
NO = 0
|
|
83
|
+
YES = 1
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
class PowerSavingMode < ABC::EnumType
|
|
87
|
+
OFF = 0
|
|
88
|
+
ON = 10
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
class AirflowControlMode < ABC::EnumType
|
|
92
|
+
EVEN = 0
|
|
93
|
+
INDIRECT = 1
|
|
94
|
+
DIRECT = 2
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
class RemoteProhibitFlags < ABC::BitMapType
|
|
98
|
+
LOCK_POWER = 0x01
|
|
99
|
+
LOCK_MODE = 0x02
|
|
100
|
+
LOCK_TEMPERATURE = 0x04
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
class StatusFlags < ABC::BitMapType
|
|
104
|
+
FILTER = 0x01
|
|
105
|
+
DEFROST = 0x02
|
|
106
|
+
HOT_ADJUST = 0x04
|
|
107
|
+
STANDBY = 0x08
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
##
|
|
111
|
+
# Update flags data type.
|
|
112
|
+
class UpdateFlags < ABC::BitMapType
|
|
113
|
+
def serialize
|
|
114
|
+
if @byte_size.zero?
|
|
115
|
+
return []
|
|
116
|
+
elsif @byte_size == 1
|
|
117
|
+
return [@value]
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
super
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def validate?(data)
|
|
124
|
+
data.is_a?(Integer)
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
##
|
|
129
|
+
# Enhanced temperature data type.
|
|
130
|
+
class EnhancedTemperature < ABC::RangeType
|
|
131
|
+
RANGE = (-64..63.5)
|
|
132
|
+
|
|
133
|
+
def self.parse(data)
|
|
134
|
+
new((data - 128) * 0.5)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def serialize
|
|
138
|
+
((@value * 2) + 128).to_i & 0xff
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
##
|
|
143
|
+
# Legacy setpoint temperature data type.
|
|
144
|
+
class LegacySetpointTemperature < ABC::RangeType
|
|
145
|
+
RANGE = (16..31.5)
|
|
146
|
+
|
|
147
|
+
def self.parse(data)
|
|
148
|
+
new(31 - (data & 0x0f) + (((data & 0x10) >> 4) * 0.5))
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def serialize
|
|
152
|
+
int_temp = (31 - @value.floor) & 0x0f
|
|
153
|
+
half_temp = ((@value % 1) >= 0.5 ? 1 : 0) << 4
|
|
154
|
+
int_temp | half_temp
|
|
155
|
+
end
|
|
156
|
+
end
|
|
157
|
+
|
|
158
|
+
##
|
|
159
|
+
# Legacy heat pump temperature data type.
|
|
160
|
+
class LegacyHeatPumpTemperature < ABC::RangeType
|
|
161
|
+
RANGE = (10..41)
|
|
162
|
+
|
|
163
|
+
def self.parse(data)
|
|
164
|
+
new(data + 10)
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def serialize
|
|
168
|
+
@value.to_i - 10
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
##
|
|
173
|
+
# Legacy room temperature data type.
|
|
174
|
+
class LegacyRoomTemperature < ABC::RangeType
|
|
175
|
+
RANGE = (8..39.5)
|
|
176
|
+
|
|
177
|
+
def self.parse(data)
|
|
178
|
+
new((data * 0.5) + 8)
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def serialize
|
|
182
|
+
((@value - 8) * 2).to_i
|
|
183
|
+
end
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
class DehumidifyValue < ABC::RangeType
|
|
187
|
+
RANGE = (0..100)
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
##
|
|
191
|
+
# Generic scalar data type.
|
|
192
|
+
class Scalar < ABC::DataType
|
|
193
|
+
def validate?(data)
|
|
194
|
+
data.is_a?(Integer)
|
|
195
|
+
end
|
|
196
|
+
end
|
|
197
|
+
end
|
|
198
|
+
end
|
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module MitsubishiHeatpump
|
|
4
|
+
module Protocol
|
|
5
|
+
ByteParam = Struct.new("ByteParam", :byte, :cls, :update_flag, :mask)
|
|
6
|
+
|
|
7
|
+
class SetSettings < ABC::Command
|
|
8
|
+
COMMAND_ID = 0x01
|
|
9
|
+
UPDATE_FLAGS_SIZE = 2
|
|
10
|
+
BYTE_VALUES = {
|
|
11
|
+
power: ByteParam.new(2, DataTypes::Power, 0x0100),
|
|
12
|
+
operating_mode: ByteParam.new(3, DataTypes::OperatingMode, 0x0200),
|
|
13
|
+
legacy_target_temp: ByteParam.new(4, DataTypes::LegacySetpointTemperature, 0x0400),
|
|
14
|
+
fan_speed: ByteParam.new(5, DataTypes::FanMode, 0x0800),
|
|
15
|
+
vertical_vane: ByteParam.new(6, DataTypes::VerticalVane, 0x1000),
|
|
16
|
+
remote_prohibit_flags: ByteParam.new(10, DataTypes::RemoteProhibitFlags, 0x4000),
|
|
17
|
+
horizontal_vane: ByteParam.new(12, DataTypes::HorizontalVane, 0x0001),
|
|
18
|
+
target_temp: ByteParam.new(13, DataTypes::EnhancedTemperature, 0x0400)
|
|
19
|
+
}.freeze
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
class GetSettings < ABC::Command
|
|
23
|
+
COMMAND_ID = 0x02
|
|
24
|
+
BYTE_VALUES = {
|
|
25
|
+
power: ByteParam.new(2, DataTypes::Power),
|
|
26
|
+
operating_mode: ByteParam.new(3, DataTypes::OperatingMode),
|
|
27
|
+
legacy_target_temp: ByteParam.new(4, DataTypes::LegacySetpointTemperature),
|
|
28
|
+
fan_speed: ByteParam.new(5, DataTypes::FanMode),
|
|
29
|
+
vertical_vane: ByteParam.new(6, DataTypes::VerticalVane),
|
|
30
|
+
remote_prohibit_flags: ByteParam.new(7, DataTypes::RemoteProhibitFlags),
|
|
31
|
+
horizontal_vane: ByteParam.new(9, DataTypes::HorizontalVane, 0, 0x0f),
|
|
32
|
+
horizontal_vane_adjustment: ByteParam.new(9, DataTypes::HorizontalVaneAdjustment, 0, 0xf0),
|
|
33
|
+
target_temp: ByteParam.new(10, DataTypes::EnhancedTemperature),
|
|
34
|
+
target_humidity: ByteParam.new(11, DataTypes::DehumidifyValue),
|
|
35
|
+
power_saving_mode: ByteParam.new(12, DataTypes::PowerSavingMode),
|
|
36
|
+
airflow_control_mode: ByteParam.new(13, DataTypes::AirflowControlMode),
|
|
37
|
+
left_vertical_vane: ByteParam.new(14, DataTypes::Scalar)
|
|
38
|
+
}.freeze
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
class GetTemperatures < ABC::Command
|
|
42
|
+
COMMAND_ID = 0x03
|
|
43
|
+
BYTE_VALUES = {
|
|
44
|
+
legacy_temp: ByteParam.new(2, DataTypes::LegacyHeatPumpTemperature),
|
|
45
|
+
outdoor_unit_temp: ByteParam.new(4, DataTypes::EnhancedTemperature),
|
|
46
|
+
current_temp: ByteParam.new(5, DataTypes::EnhancedTemperature),
|
|
47
|
+
unknown1: ByteParam.new(6, DataTypes::EnhancedTemperature),
|
|
48
|
+
unknown2: ByteParam.new(7, DataTypes::Scalar),
|
|
49
|
+
unknown3: ByteParam.new(8, DataTypes::Scalar),
|
|
50
|
+
runtime: ByteParam.new(10..12, DataTypes::Scalar)
|
|
51
|
+
}.freeze
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
class GetErrorStatus < ABC::Command
|
|
55
|
+
COMMAND_ID = 0x04
|
|
56
|
+
BYTE_VALUES = {
|
|
57
|
+
error_code: ByteParam.new(3..4, DataTypes::Scalar),
|
|
58
|
+
short_error_code: ByteParam.new(5, DataTypes::Scalar)
|
|
59
|
+
}.freeze
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
class GetTimerInfo < ABC::Command
|
|
63
|
+
COMMAND_ID = 0x05
|
|
64
|
+
BYTE_VALUES = {
|
|
65
|
+
timer_mode: ByteParam.new(2, DataTypes::TimerMode),
|
|
66
|
+
on_minutes_set: ByteParam.new(3, DataTypes::Scalar),
|
|
67
|
+
off_minutes_set: ByteParam.new(4, DataTypes::Scalar),
|
|
68
|
+
on_minutes_remaining: ByteParam.new(5, DataTypes::Scalar),
|
|
69
|
+
off_minutes_remaining: ByteParam.new(6, DataTypes::Scalar)
|
|
70
|
+
}.freeze
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
class GetOperationStatus < ABC::Command
|
|
74
|
+
COMMAND_ID = 0x06
|
|
75
|
+
BYTE_VALUES = {
|
|
76
|
+
compressor_frequency: ByteParam.new(2, DataTypes::Scalar),
|
|
77
|
+
operating: ByteParam.new(3, DataTypes::Power),
|
|
78
|
+
input_power: ByteParam.new(4..5, DataTypes::Scalar),
|
|
79
|
+
lifetime_consumption: ByteParam.new(6..7, DataTypes::Scalar),
|
|
80
|
+
unknown1: ByteParam.new(10, DataTypes::Scalar)
|
|
81
|
+
}.freeze
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
class SetRunStatus < ABC::Command
|
|
85
|
+
COMMAND_ID = 0x08
|
|
86
|
+
UPDATE_FLAGS_SIZE = 2
|
|
87
|
+
BYTE_VALUES = {
|
|
88
|
+
filter_reset: ByteParam.new(2, DataTypes::FilterReset, 0x0001),
|
|
89
|
+
dehumidify_value: ByteParam.new(3, DataTypes::DehumidifyValue, 0x0004),
|
|
90
|
+
power_saving_mode: ByteParam.new(4, DataTypes::PowerSavingMode, 0x0008),
|
|
91
|
+
airflow_control_mode: ByteParam.new(5, DataTypes::AirflowControlMode, 0x0020)
|
|
92
|
+
}.freeze
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
class GetRunStatus < ABC::Command
|
|
96
|
+
COMMAND_ID = 0x09
|
|
97
|
+
BYTE_VALUES = {
|
|
98
|
+
flags: ByteParam.new(2, DataTypes::StatusFlags),
|
|
99
|
+
actual_fan_speed: ByteParam.new(3, DataTypes::Scalar),
|
|
100
|
+
auto_mode: ByteParam.new(4, DataTypes::Scalar)
|
|
101
|
+
}.freeze
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
class SetRequest < ABC::Packet
|
|
105
|
+
PACKET_TYPE = 0x41
|
|
106
|
+
COMMAND_TYPES = {
|
|
107
|
+
SetSettings => 0x01,
|
|
108
|
+
SetRunStatus => 0x08
|
|
109
|
+
}.freeze
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
class SetResponse < ABC::Packet
|
|
113
|
+
PACKET_TYPE = 0x61
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
class GetRequest < ABC::Packet
|
|
117
|
+
PACKET_TYPE = 0x42
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
class GetResponse < ABC::Packet
|
|
121
|
+
PACKET_TYPE = 0x62
|
|
122
|
+
COMMAND_TYPES = {
|
|
123
|
+
0x02 => GetSettings,
|
|
124
|
+
0x03 => GetTemperatures,
|
|
125
|
+
0x04 => GetErrorStatus,
|
|
126
|
+
0x05 => GetTimerInfo,
|
|
127
|
+
0x06 => GetOperationStatus,
|
|
128
|
+
0x09 => GetRunStatus
|
|
129
|
+
}.freeze
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
class ConnectRequest < ABC::Packet
|
|
133
|
+
PACKET_TYPE = 0x5a
|
|
134
|
+
end
|
|
135
|
+
|
|
136
|
+
class ConnectResponse < ABC::Packet
|
|
137
|
+
PACKET_TYPE = 0x7a
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
class IdentifyRequest < ABC::Packet
|
|
141
|
+
PACKET_TYPE = 0x5b
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
class IdentifyResponse < ABC::Packet
|
|
145
|
+
PACKET_TYPE = 0x7b
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
##
|
|
149
|
+
# Protocol Data Unit (PDU) for Mitsubishi Heatpump communication.
|
|
150
|
+
class PDU
|
|
151
|
+
SYNC_BYTE = 0xfc
|
|
152
|
+
PROTOCOL_ID = [0x01, 0x30].freeze
|
|
153
|
+
PACKET_TYPES = {
|
|
154
|
+
0x41 => SetRequest,
|
|
155
|
+
0x61 => SetResponse,
|
|
156
|
+
0x42 => GetRequest,
|
|
157
|
+
0x62 => GetResponse,
|
|
158
|
+
0x5a => ConnectRequest,
|
|
159
|
+
0x7a => ConnectResponse,
|
|
160
|
+
0x5b => IdentifyRequest,
|
|
161
|
+
0x7b => IdentifyResponse
|
|
162
|
+
}.freeze
|
|
163
|
+
|
|
164
|
+
attr_reader :packet
|
|
165
|
+
|
|
166
|
+
def initialize(packet)
|
|
167
|
+
@packet = packet
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def self.parse(hex_string)
|
|
171
|
+
bytes = [hex_string].pack("H*").bytes
|
|
172
|
+
|
|
173
|
+
validate(bytes)
|
|
174
|
+
|
|
175
|
+
new(PACKET_TYPES[bytes[1]].parse(bytes[5, bytes[4]]))
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
def serialize
|
|
179
|
+
payload = @packet.serialize
|
|
180
|
+
length = payload.length
|
|
181
|
+
bytes = [SYNC_BYTE, @packet.class::PACKET_TYPE, *PROTOCOL_ID, length] + payload
|
|
182
|
+
checksum = Util.calculate_checksum(bytes)
|
|
183
|
+
bytes << checksum
|
|
184
|
+
bytes.map { |b| format("%02x", b) }.join
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
def inspect
|
|
188
|
+
"#{self.class.name.split("::").last} => #{@packet.inspect}"
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
alias to_s inspect
|
|
192
|
+
|
|
193
|
+
def self.validate(bytes)
|
|
194
|
+
raise ProtocolError, "Invalid packet" if bytes.empty? || bytes[0] != SYNC_BYTE
|
|
195
|
+
|
|
196
|
+
raise ProtocolError, "Unsupported packet type" unless PACKET_TYPES[bytes[1]]
|
|
197
|
+
|
|
198
|
+
raise ProtocolError, "Unsupported protocol" unless bytes[2..3] == PROTOCOL_ID
|
|
199
|
+
|
|
200
|
+
raise ProtocolError, "Invalid checksum" unless Util.calculate_checksum(bytes[..-2]) == bytes.last
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
class ProtocolError < StandardError; end
|
|
205
|
+
end
|
|
206
|
+
end
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "net/http"
|
|
4
|
+
require "openssl"
|
|
5
|
+
require "base64"
|
|
6
|
+
require "nokogiri"
|
|
7
|
+
require "mitsubishi_heatpump/util"
|
|
8
|
+
require "mitsubishi_heatpump/abc"
|
|
9
|
+
require "mitsubishi_heatpump/api"
|
|
10
|
+
require "mitsubishi_heatpump/data_types"
|
|
11
|
+
require "mitsubishi_heatpump/protocol"
|
metadata
ADDED
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: mitsubishi_heatpump
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Anders Alfredsson
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-02-07 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: nokogiri
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - "~>"
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '1.18'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - "~>"
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '1.18'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: bundler
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - "~>"
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '2.0'
|
|
34
|
+
type: :development
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - "~>"
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '2.0'
|
|
41
|
+
- !ruby/object:Gem::Dependency
|
|
42
|
+
name: rake
|
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - "~>"
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: '13.0'
|
|
48
|
+
type: :development
|
|
49
|
+
prerelease: false
|
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - "~>"
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: '13.0'
|
|
55
|
+
- !ruby/object:Gem::Dependency
|
|
56
|
+
name: rspec
|
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
|
58
|
+
requirements:
|
|
59
|
+
- - "~>"
|
|
60
|
+
- !ruby/object:Gem::Version
|
|
61
|
+
version: '3.0'
|
|
62
|
+
type: :development
|
|
63
|
+
prerelease: false
|
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
65
|
+
requirements:
|
|
66
|
+
- - "~>"
|
|
67
|
+
- !ruby/object:Gem::Version
|
|
68
|
+
version: '3.0'
|
|
69
|
+
- !ruby/object:Gem::Dependency
|
|
70
|
+
name: rubocop
|
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
|
72
|
+
requirements:
|
|
73
|
+
- - "~>"
|
|
74
|
+
- !ruby/object:Gem::Version
|
|
75
|
+
version: '1.82'
|
|
76
|
+
type: :development
|
|
77
|
+
prerelease: false
|
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
79
|
+
requirements:
|
|
80
|
+
- - "~>"
|
|
81
|
+
- !ruby/object:Gem::Version
|
|
82
|
+
version: '1.82'
|
|
83
|
+
description:
|
|
84
|
+
email: andersb86@gmail.com
|
|
85
|
+
executables:
|
|
86
|
+
- mitsubishi
|
|
87
|
+
extensions: []
|
|
88
|
+
extra_rdoc_files: []
|
|
89
|
+
files:
|
|
90
|
+
- bin/mitsubishi
|
|
91
|
+
- lib/mitsubishi_heatpump.rb
|
|
92
|
+
- lib/mitsubishi_heatpump/abc.rb
|
|
93
|
+
- lib/mitsubishi_heatpump/api.rb
|
|
94
|
+
- lib/mitsubishi_heatpump/data_types.rb
|
|
95
|
+
- lib/mitsubishi_heatpump/protocol.rb
|
|
96
|
+
- lib/mitsubishi_heatpump/util.rb
|
|
97
|
+
- lib/mitsubishi_heatpump/version.rb
|
|
98
|
+
homepage: https://github.com/pacive/mitsubishi_heatpump
|
|
99
|
+
licenses:
|
|
100
|
+
- MIT
|
|
101
|
+
metadata:
|
|
102
|
+
rubygems_mfa_required: 'true'
|
|
103
|
+
post_install_message:
|
|
104
|
+
rdoc_options: []
|
|
105
|
+
require_paths:
|
|
106
|
+
- lib
|
|
107
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
108
|
+
requirements:
|
|
109
|
+
- - "~>"
|
|
110
|
+
- !ruby/object:Gem::Version
|
|
111
|
+
version: '3.1'
|
|
112
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
113
|
+
requirements:
|
|
114
|
+
- - ">="
|
|
115
|
+
- !ruby/object:Gem::Version
|
|
116
|
+
version: '0'
|
|
117
|
+
requirements: []
|
|
118
|
+
rubygems_version: 3.3.26
|
|
119
|
+
signing_key:
|
|
120
|
+
specification_version: 4
|
|
121
|
+
summary: A gem for local control of Mitsubishi heat pumps
|
|
122
|
+
test_files: []
|