midea-air-condition 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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: []