midea-air-condition 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +158 -0
- data/lib/client.rb +124 -0
- data/lib/commands/command.rb +33 -0
- data/lib/commands/request_status.rb +10 -0
- data/lib/commands/set.rb +32 -0
- data/lib/device.rb +76 -0
- data/lib/midea_air_condition.rb +19 -0
- data/lib/packet_builder.rb +63 -0
- data/lib/security.rb +108 -0
- data/lib/version.rb +5 -0
- metadata +166 -0
checksums.yaml
ADDED
@@ -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.
|
data/README.md
ADDED
@@ -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
|
+
```
|
data/lib/client.rb
ADDED
@@ -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
|
data/lib/commands/set.rb
ADDED
@@ -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
|
data/lib/device.rb
ADDED
@@ -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
|
data/lib/security.rb
ADDED
@@ -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
|
data/lib/version.rb
ADDED
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: []
|