midea-air-condition 0.0.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.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: d5cebc189d6087144eae46746756a45bee886a4d
4
+ data.tar.gz: 92f8832bee21623bd30922649d606b7078a40814
5
+ SHA512:
6
+ metadata.gz: 172c3d5bc3f92ece38bf1fb34773431fe5d0de85f249e56ae5bb1dda1b157c41050e644cf73e033312e94fcea736dd35fb977ca07909a858a110dad26a3bb704
7
+ data.tar.gz: f34c1e00a63474e3096e400d0eb32fafcc8552f498196fa4bbe4c3ee95a76c6708a3a3cc5148213d05d12ddd338464ef172bd54d52b16c5695ebbf8f4b2c9e47
data/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License
2
+
3
+ Copyright (C) 2018 by Balazs Nadasdi
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
13
+ all 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
21
+ THE SOFTWARE.
@@ -0,0 +1,158 @@
1
+ ### How to use
2
+
3
+ The API key is `3742e9e5842d4ad59c2db887e12449f9` if you extract
4
+ it from their `.apk` package. But I'm not sure.
5
+
6
+ ```
7
+ require_relative 'lib/midea_air_condition'
8
+
9
+ client = MideaAirCondition::Client.new(
10
+ 'username/email',
11
+ 'passsssssword',
12
+ app_key: 'api_key'
13
+ )
14
+
15
+ client.debug = true
16
+ client.login
17
+ devices = client.appliance_list
18
+
19
+ target = 0
20
+
21
+ devices.each do |device|
22
+ print "[id=#{device['id']} type=#{device['type']}]"
23
+ print " #{device['name']} is "
24
+ print 'not ' if device['onlineStatus'] != '1'
25
+ print 'online and '
26
+ print 'not ' if device['activeStatus'] != '1'
27
+ puts 'active.'
28
+
29
+ target = device['id'] if device['onlineStatus'] == '1'
30
+ end
31
+
32
+ # Status
33
+ builder = client.new_packet_builder
34
+ command = MideaAirCondition::Command::RequestStatus.new
35
+ builder.add_command(command)
36
+
37
+ response = client.appliance_transparent_send(
38
+ target,
39
+ builder.finalize
40
+ )
41
+
42
+ device = MideaAirCondition::Device.new(response)
43
+
44
+ puts "#{target} is turned #{(device.power_status ? 'on' : 'off')}."
45
+ if device.power_status
46
+ puts 'Details:'
47
+ puts " Target temperature is #{device.temperature} celsius."
48
+ puts " // Indoor temperature: #{device.indoor_temperature} celsius."
49
+ puts " // Outdoor temperature: #{device.outdoor_temperature} celsius."
50
+ puts " Mode: #{device.mode_human}."
51
+ puts " Fan speed: #{device.fan_speed}."
52
+ puts " TimerOn is #{(device.on_timer[:status] ? '' : 'not')} active."
53
+ puts " at: #{device.on_timer_human}" if device.on_timer[:status]
54
+ puts " TimerOff is #{(device.off_timer[:status] ? '' : 'not')} active."
55
+ puts " at: #{device.off_timer_human}" if device.off_timer[:status]
56
+ puts " Eco mode is #{(device.eco ? 'on' : 'off')}."
57
+ end
58
+
59
+ # Turn on
60
+ builder = client.new_packet_builder
61
+ command = MideaAirCondition::Command::Set.new
62
+ command.turn_on
63
+ command.temperature 25
64
+ builder.add_command(command)
65
+
66
+ response = client.appliance_transparent_send(
67
+ target,
68
+ builder.finalize
69
+ )
70
+
71
+ device = MideaAirCondition::Device.new(response)
72
+ puts " Target temperature is #{device.temperature} celsius."
73
+
74
+ # Turn off
75
+ builder = client.new_packet_builder
76
+ command = MideaAirCondition::Command::Set.new
77
+ command.turn_off
78
+ builder.add_command(command)
79
+
80
+ response = client.appliance_transparent_send(
81
+ target,
82
+ builder.finalize
83
+ )
84
+ device = MideaAirCondition::Device.new(response)
85
+ puts " Target temperature is #{device.temperature} celsius."
86
+
87
+ # Set temperature to 23 celsius
88
+ builder = client.new_packet_builder
89
+ command = MideaAirCondition::Command::Set.new
90
+ command.temperature 23
91
+ builder.add_command(command)
92
+
93
+ response = client.appliance_transparent_send(
94
+ target,
95
+ builder.finalize
96
+ )
97
+
98
+ device = MideaAirCondition::Device.new(response)
99
+ puts " Target temperature is #{device.temperature} celsius."
100
+ ```
101
+
102
+ ### CRC Table + base64
103
+
104
+ I tried to generate the crc8 table, but I can't.
105
+ The table itself is a very long one,
106
+ so I packed base64 encoded it.
107
+
108
+ At least this one is not working:
109
+
110
+ ```
111
+ crc8_table = []
112
+ 256.times do |i|
113
+ crc = i
114
+ 8.times do
115
+ crc = (crc << 1) ^ (crc & 0x80 != 0 ? 0x07 : 0)
116
+ end
117
+ crc8_table[i] = crc & 0xff
118
+ end
119
+ ```
120
+
121
+ The final table should look like this:
122
+
123
+ ```
124
+ crc8_854_table = [
125
+ 0x00, 0x5E, 0xBC, 0xE2, 0x61, 0x3F, 0xDD, 0x83,
126
+ 0xC2, 0x9C, 0x7E, 0x20, 0xA3, 0xFD, 0x1F, 0x41,
127
+ 0x9D, 0xC3, 0x21, 0x7F, 0xFC, 0xA2, 0x40, 0x1E,
128
+ 0x5F, 0x01, 0xE3, 0xBD, 0x3E, 0x60, 0x82, 0xDC,
129
+ 0x23, 0x7D, 0x9F, 0xC1, 0x42, 0x1C, 0xFE, 0xA0,
130
+ 0xE1, 0xBF, 0x5D, 0x03, 0x80, 0xDE, 0x3C, 0x62,
131
+ 0xBE, 0xE0, 0x02, 0x5C, 0xDF, 0x81, 0x63, 0x3D,
132
+ 0x7C, 0x22, 0xC0, 0x9E, 0x1D, 0x43, 0xA1, 0xFF,
133
+ 0x46, 0x18, 0xFA, 0xA4, 0x27, 0x79, 0x9B, 0xC5,
134
+ 0x84, 0xDA, 0x38, 0x66, 0xE5, 0xBB, 0x59, 0x07,
135
+ 0xDB, 0x85, 0x67, 0x39, 0xBA, 0xE4, 0x06, 0x58,
136
+ 0x19, 0x47, 0xA5, 0xFB, 0x78, 0x26, 0xC4, 0x9A,
137
+ 0x65, 0x3B, 0xD9, 0x87, 0x04, 0x5A, 0xB8, 0xE6,
138
+ 0xA7, 0xF9, 0x1B, 0x45, 0xC6, 0x98, 0x7A, 0x24,
139
+ 0xF8, 0xA6, 0x44, 0x1A, 0x99, 0xC7, 0x25, 0x7B,
140
+ 0x3A, 0x64, 0x86, 0xD8, 0x5B, 0x05, 0xE7, 0xB9,
141
+ 0x8C, 0xD2, 0x30, 0x6E, 0xED, 0xB3, 0x51, 0x0F,
142
+ 0x4E, 0x10, 0xF2, 0xAC, 0x2F, 0x71, 0x93, 0xCD,
143
+ 0x11, 0x4F, 0xAD, 0xF3, 0x70, 0x2E, 0xCC, 0x92,
144
+ 0xD3, 0x8D, 0x6F, 0x31, 0xB2, 0xEC, 0x0E, 0x50,
145
+ 0xAF, 0xF1, 0x13, 0x4D, 0xCE, 0x90, 0x72, 0x2C,
146
+ 0x6D, 0x33, 0xD1, 0x8F, 0x0C, 0x52, 0xB0, 0xEE,
147
+ 0x32, 0x6C, 0x8E, 0xD0, 0x53, 0x0D, 0xEF, 0xB1,
148
+ 0xF0, 0xAE, 0x4C, 0x12, 0x91, 0xCF, 0x2D, 0x73,
149
+ 0xCA, 0x94, 0x76, 0x28, 0xAB, 0xF5, 0x17, 0x49,
150
+ 0x08, 0x56, 0xB4, 0xEA, 0x69, 0x37, 0xD5, 0x8B,
151
+ 0x57, 0x09, 0xEB, 0xB5, 0x36, 0x68, 0x8A, 0xD4,
152
+ 0x95, 0xCB, 0x29, 0x77, 0xF4, 0xAA, 0x48, 0x16,
153
+ 0xE9, 0xB7, 0x55, 0x0B, 0x88, 0xD6, 0x34, 0x6A,
154
+ 0x2B, 0x75, 0x97, 0xC9, 0x4A, 0x14, 0xF6, 0xA8,
155
+ 0x74, 0x2A, 0xC8, 0x96, 0x15, 0x4B, 0xA9, 0xF7,
156
+ 0xB6, 0xE8, 0x0A, 0x54, 0xD7, 0x89, 0x6B, 0x35
157
+ ]
158
+ ```
@@ -0,0 +1,124 @@
1
+ require 'digest/sha2'
2
+ require 'json'
3
+ require 'net/http'
4
+ require 'openssl'
5
+
6
+ module MideaAirCondition
7
+ # Client for Midea AC server
8
+ class Client
9
+ SERVER_URL = 'https://mapp.appsmb.com/v1'.freeze
10
+ CLIENT_TYPE = 1 # Android
11
+ FORMAT = 2 # JSON
12
+ LANGUAGE = 'en_US'.freeze
13
+
14
+ attr_accessor :debug
15
+
16
+ def initialize(email, password, app_key:, app_id: 1017, src: 17)
17
+ @app_id = app_id
18
+ @src = src
19
+ @email = email
20
+ @password = password
21
+ @app_key = app_key
22
+ @debug = false
23
+
24
+ @security = Security.new(app_key: @app_key)
25
+
26
+ @current = nil
27
+ end
28
+
29
+ def login
30
+ login_id = user_login_id_get['loginId']
31
+
32
+ encrypted_password = @security.encrypt_password(@password, login_id)
33
+ @current = api_request(
34
+ 'user/login',
35
+ loginAccount: @email,
36
+ password: encrypted_password
37
+ )
38
+ @security.access_token = @current['accessToken']
39
+ end
40
+
41
+ def appliance_list
42
+ response = api_request(
43
+ 'appliance/list/get',
44
+ homegroupId: default_home['id']
45
+ )
46
+ response['list']
47
+ end
48
+
49
+ def appliance_transparent_send(appliance_id, data)
50
+ response = api_request(
51
+ 'appliance/transparent/send',
52
+ order: encode(@security.transcode(data).join(',')),
53
+ funId: '0000',
54
+ applianceId: appliance_id
55
+ )
56
+
57
+ response = decode(response['reply']).split(',').map { |p| p.to_i & 0xff }
58
+
59
+ response
60
+ end
61
+
62
+ def new_packet_builder
63
+ PacketBuilder.new(@security)
64
+ end
65
+
66
+ private
67
+
68
+ def encode(data)
69
+ @security.aes_encrypt(data, @security.data_key)
70
+ end
71
+
72
+ def decode(data)
73
+ @security.aes_decrypt(data, @security.data_key)
74
+ end
75
+
76
+ def api_request(endpoint, **args)
77
+ args = {
78
+ appId: @app_id, format: FORMAT, clientType: CLIENT_TYPE,
79
+ language: LANGUAGE, src: @src,
80
+ stamp: Time.now.strftime('%Y%m%d%H%M%S')
81
+ }.merge(args)
82
+
83
+ args[:sessionId] = @current['sessionId'] unless @current.nil?
84
+
85
+ path = "/#{SERVER_URL.split('/').last}/#{endpoint}"
86
+ args[:sign] = @security.sign(path, args)
87
+
88
+ result = send_api_request(URI("#{SERVER_URL}/#{endpoint}"), args)
89
+
90
+ result['result']
91
+ end
92
+
93
+ def send_api_request(uri, args)
94
+ Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
95
+ request = Net::HTTP::Post.new(uri)
96
+ request.set_form_data(args)
97
+
98
+ result = JSON.parse(http.request(request).body)
99
+ raise result['msg'] unless result['errorCode'] == '0'
100
+
101
+ log(result['result'])
102
+ result
103
+ end
104
+ end
105
+
106
+ def user_login_id_get
107
+ api_request('user/login/id/get', loginAccount: @email)
108
+ end
109
+
110
+ def default_home
111
+ @default_home ||= api_home_list['list'].select do |h|
112
+ h['isDefault'].to_i == 1
113
+ end.first
114
+ end
115
+
116
+ def api_home_list
117
+ api_request('homegroup/list/get')
118
+ end
119
+
120
+ def log(data)
121
+ p data if @debug
122
+ end
123
+ end
124
+ end
@@ -0,0 +1,33 @@
1
+ module MideaAirCondition
2
+ module Command
3
+ # Base Command class
4
+ class BaseCommand
5
+ # Default device type: 0xAC
6
+ def initialize(device_type: 0xAC)
7
+ @data = [0xaa, 0x23, device_type, 0x00, 0x00, 0x00, 0x00, 0x00]
8
+
9
+ @data += [
10
+ 0x03, 0x02, 0xff, 0x81, 0x00, 0xff, 0x03, 0xff,
11
+ 0x00, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
12
+ 0x00, 0x00, 0x00, 0x00
13
+ ]
14
+
15
+ fill
16
+ end
17
+
18
+ def finalize(security)
19
+ # Add command sequence number
20
+ # Can't be lower than 3
21
+ @data << 0x03
22
+ @data << security.crc8(@data[0x10..(@data.length - 1)])
23
+ @data[0x01] = @data.length
24
+
25
+ @data
26
+ end
27
+
28
+ private
29
+
30
+ def fill; end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,10 @@
1
+ module MideaAirCondition
2
+ module Command
3
+ # Request status of a device
4
+ class RequestStatus < BaseCommand
5
+ def fill
6
+ @data[0x0a] = 0x40
7
+ end
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,32 @@
1
+ module MideaAirCondition
2
+ module Command
3
+ # Request status of a device
4
+ class Set < BaseCommand
5
+ def turn_on
6
+ @data[0x0b] = 0x43
7
+ end
8
+
9
+ def turn_off
10
+ @data[0x0b] = 0x42
11
+ end
12
+
13
+ def temperature(celsius, mode: 2)
14
+ c = ((mode << 5) & 0xe0) | (celsius & 0xf) | ((celsius << 4) & 0x10)
15
+ @data[0x0c] = c
16
+ end
17
+
18
+ def fan_speed(speed)
19
+ @data[0x0d] = speed
20
+ end
21
+
22
+ private
23
+
24
+ def fill
25
+ @data[0x0a] = 0x40
26
+
27
+ temperature 22
28
+ fan_speed 40
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,76 @@
1
+ module MideaAirCondition
2
+ # Device representation (now only for status parsing)
3
+ class Device
4
+ attr_reader :data
5
+
6
+ def initialize(data)
7
+ @data = data
8
+ @pointer = 0x33
9
+ end
10
+
11
+ def power_status
12
+ !(@data[@pointer] & 0x01).zero?
13
+ end
14
+
15
+ def temperature
16
+ (@data[@pointer + 1] & 0xf) + 16
17
+ end
18
+
19
+ def mode
20
+ (@data[@pointer + 1] & 0xe0) >> 5
21
+ end
22
+
23
+ def mode_human
24
+ mode_value = 'unknown'
25
+ mode_value = 'auto' if mode == 1
26
+ mode_value = 'cool' if mode == 2
27
+ mode_value = 'dry' if mode == 3
28
+ mode_value = 'heat' if mode == 4
29
+ mode_value = 'fan' if mode == 5
30
+
31
+ mode_value
32
+ end
33
+
34
+ def fan_speed
35
+ @data[@pointer + 2] & 0x7f
36
+ end
37
+
38
+ def indoor_temperature
39
+ (@data[@pointer + 10] - 50) / 2
40
+ end
41
+
42
+ def outdoor_temperature
43
+ (@data[@pointer + 11] - 50) / 2
44
+ end
45
+
46
+ def eco
47
+ !((@data[@pointer + 8] & 0x10) >> 4).zero?
48
+ end
49
+
50
+ def on_timer
51
+ value = @data[@pointer + 3]
52
+ {
53
+ status: !((value & 0x80) >> 7).zero?,
54
+ hour: ((value & 0x7c) >> 2),
55
+ minutes: ((value & 0x3) | ((value & 0xf0)))
56
+ }
57
+ end
58
+
59
+ def off_timer
60
+ value = @data[@pointer + 4]
61
+ {
62
+ status: !((value & 0x80) >> 7).zero?,
63
+ hour: ((value & 0x7c) >> 2),
64
+ minutes: ((value & 0x3) | ((value & 0xf0)))
65
+ }
66
+ end
67
+
68
+ def on_timer_human
69
+ "#{on_timer[:hours]}:#{on_timer[:mins]}"
70
+ end
71
+
72
+ def off_timer_human
73
+ "#{off_timer[:hours]}:#{off_timer[:mins]}"
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,19 @@
1
+ require_relative 'client'
2
+ require_relative 'device'
3
+ require_relative 'packet_builder'
4
+ require_relative 'security'
5
+ require_relative 'version'
6
+
7
+ # MideaAirCondition namespace
8
+ module MideaAirCondition
9
+ # Module to deparate command classes
10
+ module Command
11
+ end
12
+ end
13
+
14
+ path = File.join(File.dirname(__FILE__), 'commands', 'command.rb')
15
+ require path
16
+ path = File.join(File.dirname(__FILE__), 'commands', '*.rb')
17
+ Dir.glob(path).each do |file|
18
+ require file
19
+ end
@@ -0,0 +1,63 @@
1
+ module MideaAirCondition
2
+ # This is where we build our packets
3
+ class PacketBuilder
4
+ def initialize(security)
5
+ @security = security
6
+ @command = []
7
+
8
+ populate_header_data
9
+
10
+ # Maybe this one is the client id
11
+ # In a response it's the device id
12
+ # and the first six bytes are the same
13
+ @packet += [0xc6, 0x79, 0x00, 0x00, 0x00, 0x05, 0x0a, 0x00]
14
+
15
+ add_unknown_section
16
+ end
17
+
18
+ def add_command(command)
19
+ raise Exception, 'Invalid argument' if command.is_a?(Command)
20
+ @command += command.finalize(@security)
21
+ end
22
+
23
+ def finalize
24
+ @packet += @command
25
+ @packet << @security.checksum(@command[1..(@command.length - 1)])
26
+ @packet << 0x00
27
+
28
+ # Add padding + update packet length
29
+ @packet += [0] * (44 - @command.length)
30
+ @packet += [0]
31
+ @packet[0x04] = @packet.length
32
+
33
+ p @packet
34
+
35
+ @packet
36
+ end
37
+
38
+ def populate_header_data
39
+ # was always fix for me except the length byte
40
+ @packet = [0x5a, 0x5a, 0x01, 0x11, 0x5c, 0x00, 0x20, 0x00]
41
+
42
+ # was different for status and power
43
+ # Set Temp
44
+ # @packet += [0x12, 0x00, 0x00, 0x00, 0x6f, 0x33, 0x0c, 0x00]
45
+ # Status
46
+ # @packet += [0x01, 0x00, 0x00, 0x00, 0x8d, 0x0f, 0x17, 0x02]
47
+ # Power
48
+ # @packet += [0x12, 0x00, 0x00, 0x00, 0x6f, 0x33, 0x0c, 0x00]
49
+ # now just use 0x00 * 8
50
+ @packet += [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]
51
+
52
+ # was always fix for me
53
+ @packet += [0x0e, 0x03, 0x12, 0x14]
54
+ end
55
+
56
+ def add_unknown_section
57
+ @packet += [
58
+ 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
59
+ 0x02, 0x00, 0x00, 0x00
60
+ ]
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,108 @@
1
+ require 'base64'
2
+
3
+ module MideaAirCondition
4
+ # Class to manage encryptions/decryptions/sign/etc
5
+ class Security
6
+ attr_accessor :access_token, :app_key
7
+
8
+ def initialize(app_key:, access_token: '')
9
+ @app_key = app_key
10
+ @access_token = access_token
11
+
12
+ @crc8_854_table = Base64.decode64(
13
+ 'AF684mE/3YPCnH4go/0fQZ3DIX/8okAeXwHjvT5ggtwjfZ/BQhz+oOG/XQOA' \
14
+ '3jxivuACXN+BYz18IsCeHUOh/0YY+qQneZvFhNo4ZuW7WQfbhWc5uuQGWBlH' \
15
+ 'pft4JsSaZTvZhwRauOan+RtFxph6JPimRBqZxyV7OmSG2FsF57mM0jBu7bNR' \
16
+ 'D04Q8qwvcZPNEU+t83AuzJLTjW8xsuwOUK/xE03OkHIsbTPRjwxSsO4ybI7Q' \
17
+ 'Uw3vsfCuTBKRzy1zypR2KKv1F0kIVrTqaTfVi1cJ67U2aIrUlcspd/SqSBbp' \
18
+ 't1ULiNY0ait1l8lKFPaodCrIlhVLqfe26ApU14lrNQ=='
19
+ ).unpack('C*')
20
+ end
21
+
22
+ def sign(path, args)
23
+ query = args.map { |k, v| "#{k}=#{v}" }.to_a.sort.join('&')
24
+ content = "#{path}#{query}#{@app_key}"
25
+ (::Digest::SHA2.new << content).to_s
26
+ end
27
+
28
+ def encrypt_password(password, login_id)
29
+ pass = (::Digest::SHA2.new << password).to_s
30
+ (::Digest::SHA2.new << "#{login_id}#{pass}#{@app_key}").to_s
31
+ end
32
+
33
+ def data_key
34
+ aes_decrypt(
35
+ @access_token,
36
+ (::Digest::MD5.new << @app_key).to_s[0...16]
37
+ )
38
+ end
39
+
40
+ def transcode(data)
41
+ data.map do |d|
42
+ (d >= 128 ? d - 256 : d)
43
+ end
44
+ end
45
+
46
+ def crc8(data)
47
+ crc_value = 0
48
+ data.each do |m|
49
+ k = crc_value ^ m
50
+ k -= 256 if k > 256
51
+ k += 256 if k < 0
52
+ crc_value = @crc8_854_table[k]
53
+ end
54
+
55
+ crc_value
56
+ end
57
+
58
+ def checksum(data)
59
+ sum_value = data.inject(&:+)
60
+ 255 - sum_value % 256 + 1
61
+ end
62
+
63
+ def aes_decrypt(data, key)
64
+ aes = OpenSSL::Cipher.new('aes128')
65
+ aes.decrypt
66
+ aes.padding = 0
67
+ aes.key = key
68
+
69
+ data = [data].pack('H*')
70
+
71
+ blocks = data.chars.each_slice(16).map(&:join)
72
+
73
+ final = ''
74
+ blocks.each do |b|
75
+ aes.reset
76
+ final += aes.update(b) + aes.final
77
+ end
78
+
79
+ pad = final[final.length - 1].ord
80
+
81
+ final[0...(final.length - pad)]
82
+ end
83
+
84
+ def aes_encrypt(data, key)
85
+ aes = OpenSSL::Cipher.new('aes128')
86
+ aes.encrypt
87
+ aes.padding = 0
88
+ aes.key = key
89
+
90
+ blocks = data.chars.each_slice(16).map(&:join)
91
+ if blocks.last.length < 16
92
+ pad = 16 - blocks.last.length
93
+ blocks[blocks.length - 1] = blocks.last + pad.chr * pad
94
+ else
95
+ pad = 16
96
+ blocks << pad.chr * pad
97
+ end
98
+
99
+ final = ''
100
+ blocks.each do |b|
101
+ aes.reset
102
+ final += aes.update(b) + aes.final
103
+ end
104
+
105
+ final.unpack('H*').first
106
+ end
107
+ end
108
+ end
@@ -0,0 +1,5 @@
1
+ module MideaAirCondition
2
+ # Current version to publish
3
+ # Gemspec also uses this constant
4
+ VERSION = '0.0.1'.freeze
5
+ end
metadata ADDED
@@ -0,0 +1,166 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: midea-air-condition
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1
5
+ platform: ruby
6
+ authors:
7
+ - Balazs Nadasdi
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2018-04-25 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: json
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 2.1.0
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: 2.1.0
27
+ - !ruby/object:Gem::Dependency
28
+ name: openssl
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: 2.0.3
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: 2.0.3
41
+ - !ruby/object:Gem::Dependency
42
+ name: rake
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '12.0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '12.0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rdoc
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '5.1'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '5.1'
69
+ - !ruby/object:Gem::Dependency
70
+ name: rspec
71
+ requirement: !ruby/object:Gem::Requirement
72
+ requirements:
73
+ - - ">="
74
+ - !ruby/object:Gem::Version
75
+ version: '3.5'
76
+ type: :development
77
+ prerelease: false
78
+ version_requirements: !ruby/object:Gem::Requirement
79
+ requirements:
80
+ - - ">="
81
+ - !ruby/object:Gem::Version
82
+ version: '3.5'
83
+ - !ruby/object:Gem::Dependency
84
+ name: rubocop
85
+ requirement: !ruby/object:Gem::Requirement
86
+ requirements:
87
+ - - ">="
88
+ - !ruby/object:Gem::Version
89
+ version: '0.48'
90
+ type: :development
91
+ prerelease: false
92
+ version_requirements: !ruby/object:Gem::Requirement
93
+ requirements:
94
+ - - ">="
95
+ - !ruby/object:Gem::Version
96
+ version: '0.48'
97
+ - !ruby/object:Gem::Dependency
98
+ name: sinatra
99
+ requirement: !ruby/object:Gem::Requirement
100
+ requirements:
101
+ - - ">="
102
+ - !ruby/object:Gem::Version
103
+ version: '1.4'
104
+ type: :development
105
+ prerelease: false
106
+ version_requirements: !ruby/object:Gem::Requirement
107
+ requirements:
108
+ - - ">="
109
+ - !ruby/object:Gem::Version
110
+ version: '1.4'
111
+ - !ruby/object:Gem::Dependency
112
+ name: webmock
113
+ requirement: !ruby/object:Gem::Requirement
114
+ requirements:
115
+ - - ">="
116
+ - !ruby/object:Gem::Version
117
+ version: '3.0'
118
+ type: :development
119
+ prerelease: false
120
+ version_requirements: !ruby/object:Gem::Requirement
121
+ requirements:
122
+ - - ">="
123
+ - !ruby/object:Gem::Version
124
+ version: '3.0'
125
+ description: API client for Midea AC systems
126
+ email: balazs.nadasdi@cheppers.com
127
+ executables: []
128
+ extensions: []
129
+ extra_rdoc_files: []
130
+ files:
131
+ - LICENSE
132
+ - README.md
133
+ - lib/client.rb
134
+ - lib/commands/command.rb
135
+ - lib/commands/request_status.rb
136
+ - lib/commands/set.rb
137
+ - lib/device.rb
138
+ - lib/midea_air_condition.rb
139
+ - lib/packet_builder.rb
140
+ - lib/security.rb
141
+ - lib/version.rb
142
+ homepage: ''
143
+ licenses:
144
+ - MIT
145
+ metadata: {}
146
+ post_install_message:
147
+ rdoc_options: []
148
+ require_paths:
149
+ - lib
150
+ required_ruby_version: !ruby/object:Gem::Requirement
151
+ requirements:
152
+ - - ">="
153
+ - !ruby/object:Gem::Version
154
+ version: '2.0'
155
+ required_rubygems_version: !ruby/object:Gem::Requirement
156
+ requirements:
157
+ - - ">="
158
+ - !ruby/object:Gem::Version
159
+ version: '0'
160
+ requirements: []
161
+ rubyforge_project:
162
+ rubygems_version: 2.6.14
163
+ signing_key:
164
+ specification_version: 4
165
+ summary: Ruby gem to communicate with Midea AirCondition systems
166
+ test_files: []