milight-v6 0.1.1 → 0.2.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 +5 -5
- data/.rubocop.yml +3 -4
- data/.travis.yml +6 -5
- data/Gemfile.lock +18 -18
- data/README.md +37 -5
- data/bin/milight +36 -21
- data/lib/milight/v6/all.rb +19 -14
- data/lib/milight/v6/bridge.rb +59 -0
- data/lib/milight/v6/command.rb +41 -51
- data/lib/milight/v6/controller.rb +17 -2
- data/lib/milight/v6/discover.rb +36 -0
- data/lib/milight/v6/socket.rb +37 -13
- data/lib/milight/v6/version.rb +1 -1
- data/lib/milight/v6/zone.rb +21 -16
- data/milight-v6.gemspec +2 -2
- metadata +12 -11
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: b43dde6c8b92454a3ebb63b83678cc709d959f3cd2aab090e4b52bc5e6ad4202
|
4
|
+
data.tar.gz: 2565cbe4cfc87572d8eb73393f3dc5459b9e6894c8ab0f2f081ac1e42c5c2937
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 7fbfbc54f039ea09e879c72b8b6dd450633bbf3f91ba98f9cdaa9c93f44b0da7a510b1bcd6fc37120d2b01e3ecb3d7bc9fd468d102108355abd16e6936edc99b
|
7
|
+
data.tar.gz: 6d7faba2778786f8b5942837cc211e53d1fab2547a1cd7fd822431b6e993031ae4105a52968d1dea766ead271f49e904ee11d195a95634477fdcfd2580138db9
|
data/.rubocop.yml
CHANGED
@@ -1,9 +1,8 @@
|
|
1
1
|
AllCops:
|
2
|
-
TargetRubyVersion: 2.
|
2
|
+
TargetRubyVersion: 2.4
|
3
3
|
DisplayCopNames: true
|
4
4
|
DisplayStyleGuide: true
|
5
5
|
Exclude:
|
6
|
-
- 'lib/milight/v6/api/version.rb'
|
7
6
|
- 'vendor/**/*'
|
8
7
|
|
9
8
|
Metrics/BlockLength:
|
@@ -13,10 +12,10 @@ Metrics/BlockLength:
|
|
13
12
|
Metrics/LineLength:
|
14
13
|
Enabled: false
|
15
14
|
|
16
|
-
|
15
|
+
Metrics/MethodLength:
|
17
16
|
Enabled: false
|
18
17
|
|
19
|
-
Style/
|
18
|
+
Style/ConditionalAssignment:
|
20
19
|
Enabled: false
|
21
20
|
|
22
21
|
Style/For:
|
data/.travis.yml
CHANGED
data/Gemfile.lock
CHANGED
@@ -1,32 +1,32 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
milight-v6 (0.
|
4
|
+
milight-v6 (0.2.0)
|
5
5
|
|
6
6
|
GEM
|
7
7
|
remote: https://rubygems.org/
|
8
8
|
specs:
|
9
9
|
ast (2.3.0)
|
10
|
-
diff-lcs (1.
|
10
|
+
diff-lcs (1.4.4)
|
11
11
|
parallel (1.12.1)
|
12
12
|
parser (2.4.0.2)
|
13
13
|
ast (~> 2.3)
|
14
14
|
powerpack (0.1.1)
|
15
15
|
rainbow (3.0.0)
|
16
|
-
rake (
|
17
|
-
rspec (3.
|
18
|
-
rspec-core (~> 3.
|
19
|
-
rspec-expectations (~> 3.
|
20
|
-
rspec-mocks (~> 3.
|
21
|
-
rspec-core (3.
|
22
|
-
rspec-support (~> 3.
|
23
|
-
rspec-expectations (3.
|
16
|
+
rake (13.0.3)
|
17
|
+
rspec (3.10.0)
|
18
|
+
rspec-core (~> 3.10.0)
|
19
|
+
rspec-expectations (~> 3.10.0)
|
20
|
+
rspec-mocks (~> 3.10.0)
|
21
|
+
rspec-core (3.10.1)
|
22
|
+
rspec-support (~> 3.10.0)
|
23
|
+
rspec-expectations (3.10.1)
|
24
24
|
diff-lcs (>= 1.2.0, < 2.0)
|
25
|
-
rspec-support (~> 3.
|
26
|
-
rspec-mocks (3.
|
25
|
+
rspec-support (~> 3.10.0)
|
26
|
+
rspec-mocks (3.10.2)
|
27
27
|
diff-lcs (>= 1.2.0, < 2.0)
|
28
|
-
rspec-support (~> 3.
|
29
|
-
rspec-support (3.
|
28
|
+
rspec-support (~> 3.10.0)
|
29
|
+
rspec-support (3.10.2)
|
30
30
|
rubocop (0.52.1)
|
31
31
|
parallel (~> 1.10)
|
32
32
|
parser (>= 2.4.0.2, < 3.0)
|
@@ -35,17 +35,17 @@ GEM
|
|
35
35
|
ruby-progressbar (~> 1.7)
|
36
36
|
unicode-display_width (~> 1.0, >= 1.0.1)
|
37
37
|
ruby-progressbar (1.9.0)
|
38
|
-
unicode-display_width (1.
|
38
|
+
unicode-display_width (1.7.0)
|
39
39
|
|
40
40
|
PLATFORMS
|
41
41
|
ruby
|
42
42
|
|
43
43
|
DEPENDENCIES
|
44
|
-
bundler (~>
|
44
|
+
bundler (~> 2.0)
|
45
45
|
milight-v6!
|
46
|
-
rake (~>
|
46
|
+
rake (~> 13.0)
|
47
47
|
rspec (~> 3.0)
|
48
48
|
rubocop (~> 0.52.1)
|
49
49
|
|
50
50
|
BUNDLED WITH
|
51
|
-
|
51
|
+
2.2.15
|
data/README.md
CHANGED
@@ -4,7 +4,10 @@
|
|
4
4
|
[](https://travis-ci.org/ppostma/milight-v6-api)
|
5
5
|
[](https://codeclimate.com/github/ppostma/milight-v6-api)
|
6
6
|
|
7
|
-
This gem provides a Ruby API for the
|
7
|
+
This gem provides a Ruby API for the Mi-Light Wifi Bridge using protocol version 6.
|
8
|
+
|
9
|
+
Supported devices are the Mi-Light WiFi iBox models 1 and 2. The [esp8266_milight_hub](https://github.com/sidoh/esp8266_milight_hub) should also work, but I haven't tested this yet.
|
10
|
+
The bridges sold under the brand MiBoxer (such as model WL-Box1) are not supported by this gem.
|
8
11
|
|
9
12
|
## Installation
|
10
13
|
|
@@ -24,20 +27,46 @@ Or install it yourself as:
|
|
24
27
|
|
25
28
|
## Usage
|
26
29
|
|
30
|
+
### Connecting to a Mi-Light controller
|
31
|
+
|
32
|
+
Connect to a Mi-Light controller by creating an instance of `Milight::V6::Controller` and supplying the IP address of the Mi-Light Wifi Bridge. If you don't know the IP address, you can use the class method `search` to discover devices on the local network.
|
33
|
+
|
27
34
|
```ruby
|
28
35
|
require "milight/v6"
|
29
36
|
|
30
37
|
controller = Milight::V6::Controller.new("192.168.178.33")
|
38
|
+
|
39
|
+
controllers = Milight::V6::Controller.search
|
40
|
+
```
|
41
|
+
|
42
|
+
### Sending commands
|
43
|
+
|
44
|
+
First select what you want control: the bridge lamp, a specific zone or all zones. Then you can start sending commands. See [Milight::V6::All](lib/milight/v6/all.rb), [Milight::V6::Bridge](lib/milight/v6/bridge.rb) and [Milight::V6::Zone](lib/milight/v6/zone.rb) for the supported commands.
|
45
|
+
|
46
|
+
Some examples:
|
47
|
+
|
48
|
+
```ruby
|
31
49
|
controller.zone(1).on
|
32
50
|
|
33
|
-
controller.zone(2).
|
51
|
+
controller.zone(2).on
|
52
|
+
controller.zone(2).warm_light.brightness(70)
|
34
53
|
|
35
|
-
controller.zone(3).
|
54
|
+
controller.zone(3).on
|
55
|
+
controller.zone(3).hue(Milight::V6::Color::BLUE).saturation(10)
|
56
|
+
|
57
|
+
controller.bridge.on
|
58
|
+
controller.bridge.brightness(50)
|
36
59
|
|
37
60
|
controller.all.off
|
38
61
|
```
|
39
62
|
|
40
|
-
|
63
|
+
The commands will be sent with an interval of 100ms, to prevent commands being dropped by the controller. You can change or disable this by setting the `wait` parameter when creating an instance of `Milight::V6::Controller`, for example:
|
64
|
+
|
65
|
+
```ruby
|
66
|
+
controller = Milight::V6::Controller.new("192.168.178.33", wait: false) # don't delay commands
|
67
|
+
|
68
|
+
controller = Milight::V6::Controller.new("192.168.178.33", wait: 0.05) # delay commands for 50 ms
|
69
|
+
```
|
41
70
|
|
42
71
|
## Command line
|
43
72
|
|
@@ -45,9 +74,12 @@ A command line tool is included which can be used to control the lights.
|
|
45
74
|
|
46
75
|
Usage: milight <host> <command> [zone]
|
47
76
|
|
48
|
-
Supported commands: on, off, link, unlink
|
77
|
+
Supported commands: search, on, off, link, unlink
|
78
|
+
|
79
|
+
Examples:
|
49
80
|
|
50
81
|
```bash
|
82
|
+
$ milight search # search for devices
|
51
83
|
$ milight 192.168.178.33 on 1 # turn on lights for zone 1
|
52
84
|
$ milight 192.168.178.33 off # turn off lights for all zones
|
53
85
|
```
|
data/bin/milight
CHANGED
@@ -3,30 +3,45 @@
|
|
3
3
|
|
4
4
|
require "milight/v6"
|
5
5
|
|
6
|
-
if ARGV.length < 2
|
7
|
-
puts "Usage: #{$PROGRAM_NAME} <host> <command> [zone]"
|
8
|
-
exit 1
|
9
|
-
end
|
10
|
-
|
11
6
|
host = ARGV.shift
|
12
|
-
command = ARGV.shift
|
13
|
-
zone = ARGV.shift
|
14
7
|
|
15
|
-
|
8
|
+
if host == "search"
|
9
|
+
controllers = Milight::V6::Controller.search
|
16
10
|
|
17
|
-
if
|
18
|
-
|
11
|
+
if !controllers.empty?
|
12
|
+
controllers.each { |c| puts c.to_s }
|
13
|
+
else
|
14
|
+
puts "No Mi-Light devices found."
|
15
|
+
end
|
19
16
|
else
|
20
|
-
|
21
|
-
|
17
|
+
if ARGV.empty?
|
18
|
+
puts "Usage: #{$PROGRAM_NAME} <host> <command> [zone]"
|
19
|
+
puts " #{$PROGRAM_NAME} search"
|
20
|
+
exit 1
|
21
|
+
end
|
22
|
+
|
23
|
+
command = ARGV.shift
|
24
|
+
zone = ARGV.shift
|
25
|
+
|
26
|
+
controller = Milight::V6::Controller.new(host)
|
27
|
+
|
28
|
+
case zone
|
29
|
+
when nil
|
30
|
+
lights = controller.all
|
31
|
+
when "bridge"
|
32
|
+
lights = controller.bridge
|
33
|
+
else
|
34
|
+
lights = controller.zone(zone.to_i)
|
35
|
+
end
|
22
36
|
|
23
|
-
case command
|
24
|
-
when "link"
|
25
|
-
|
26
|
-
when "unlink"
|
27
|
-
|
28
|
-
when "off"
|
29
|
-
|
30
|
-
when "on"
|
31
|
-
|
37
|
+
case command
|
38
|
+
when "link"
|
39
|
+
lights.link
|
40
|
+
when "unlink"
|
41
|
+
lights.unlink
|
42
|
+
when "off"
|
43
|
+
lights.off
|
44
|
+
when "on"
|
45
|
+
lights.on
|
46
|
+
end
|
32
47
|
end
|
data/lib/milight/v6/all.rb
CHANGED
@@ -2,6 +2,7 @@
|
|
2
2
|
|
3
3
|
module Milight
|
4
4
|
module V6
|
5
|
+
# Commands for lamps in all zones.
|
5
6
|
class All
|
6
7
|
def initialize(command)
|
7
8
|
@command = command
|
@@ -9,64 +10,68 @@ module Milight
|
|
9
10
|
|
10
11
|
# Switch the lights on.
|
11
12
|
def on
|
12
|
-
@command.
|
13
|
+
@command.execute(0, [0x31, 0x00, 0x00, 0x08, 0x04, 0x01, 0x00, 0x00, 0x00])
|
13
14
|
|
14
15
|
self
|
15
16
|
end
|
16
17
|
|
17
18
|
# Switch the lights off.
|
18
19
|
def off
|
19
|
-
@command.
|
20
|
+
@command.execute(0, [0x31, 0x00, 0x00, 0x08, 0x04, 0x02, 0x00, 0x00, 0x00])
|
20
21
|
|
21
22
|
self
|
22
23
|
end
|
23
24
|
|
24
25
|
# Enable night light mode.
|
25
26
|
def night_light
|
26
|
-
@command.
|
27
|
+
@command.execute(0, [0x31, 0x00, 0x00, 0x08, 0x04, 0x05, 0x00, 0x00, 0x00])
|
27
28
|
|
28
29
|
self
|
29
30
|
end
|
30
31
|
|
31
|
-
# Set brightness, value: 0% to 100
|
32
|
+
# Set brightness, value: 0% to 100%.
|
32
33
|
def brightness(value)
|
33
|
-
|
34
|
+
raise ArgumentError, "Please supply a brightness value between 0-100." if value.negative? || value > 100
|
35
|
+
|
36
|
+
@command.execute(0, [0x31, 0x00, 0x00, 0x08, 0x03, value, 0x00, 0x00, 0x00])
|
34
37
|
|
35
38
|
self
|
36
39
|
end
|
37
40
|
|
38
41
|
# Set color temperature, value: 0 = 2700K, 100 = 6500K.
|
39
42
|
def temperature(value)
|
40
|
-
|
43
|
+
raise ArgumentError, "Please supply a temperature value between 0-100 (2700K to 6500K)." if value.negative? || value > 100
|
44
|
+
|
45
|
+
@command.execute(0, [0x31, 0x00, 0x00, 0x08, 0x05, value, 0x00, 0x00, 0x00])
|
41
46
|
|
42
47
|
self
|
43
48
|
end
|
44
49
|
|
45
50
|
# Set color temperature to warm light (2700K).
|
46
51
|
def warm_light
|
47
|
-
|
48
|
-
|
49
|
-
self
|
52
|
+
temperature(0)
|
50
53
|
end
|
51
54
|
|
52
55
|
# Set color temperature to white (cool) light (6500K).
|
53
56
|
def white_light
|
54
|
-
|
55
|
-
|
56
|
-
self
|
57
|
+
temperature(100)
|
57
58
|
end
|
58
59
|
|
59
60
|
# Set the hue, value: 0 to 255 (red).
|
60
61
|
# See Milight::V6::Color for predefined colors.
|
61
62
|
def hue(value)
|
62
|
-
|
63
|
+
raise ArgumentError, "Please supply a hue value between 0-255." if value.negative? || value > 255
|
64
|
+
|
65
|
+
@command.execute(0, [0x31, 0x00, 0x00, 0x08, 0x01, value, value, value, value])
|
63
66
|
|
64
67
|
self
|
65
68
|
end
|
66
69
|
|
67
70
|
# Set the saturation, value: 0% to 100%.
|
68
71
|
def saturation(value)
|
69
|
-
|
72
|
+
raise ArgumentError, "Please supply a saturation value between 0-100." if value.negative? || value > 100
|
73
|
+
|
74
|
+
@command.execute(0, [0x31, 0x00, 0x00, 0x08, 0x02, value, 0x00, 0x00, 0x00])
|
70
75
|
|
71
76
|
self
|
72
77
|
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Milight
|
4
|
+
module V6
|
5
|
+
# Commands for the bridge lamp (iBox model 1).
|
6
|
+
class Bridge
|
7
|
+
def initialize(command)
|
8
|
+
@command = command
|
9
|
+
end
|
10
|
+
|
11
|
+
# Switch the light on.
|
12
|
+
def on
|
13
|
+
@command.execute(1, [0x31, 0x00, 0x00, 0x00, 0x03, 0x03, 0x00, 0x00, 0x00])
|
14
|
+
|
15
|
+
self
|
16
|
+
end
|
17
|
+
|
18
|
+
# Switch the light off.
|
19
|
+
def off
|
20
|
+
@command.execute(1, [0x31, 0x00, 0x00, 0x00, 0x03, 0x04, 0x00, 0x00, 0x00])
|
21
|
+
|
22
|
+
self
|
23
|
+
end
|
24
|
+
|
25
|
+
# Set brightness, value: 0% to 100%.
|
26
|
+
def brightness(value)
|
27
|
+
raise ArgumentError, "Please supply a brightness value between 0-100." if value.negative? || value > 100
|
28
|
+
|
29
|
+
@command.execute(1, [0x31, 0x00, 0x00, 0x00, 0x02, value, 0x00, 0x00, 0x00])
|
30
|
+
|
31
|
+
self
|
32
|
+
end
|
33
|
+
|
34
|
+
# Set color to white light.
|
35
|
+
def white_light
|
36
|
+
@command.execute(1, [0x31, 0x00, 0x00, 0x00, 0x03, 0x05, 0x00, 0x00, 0x00])
|
37
|
+
|
38
|
+
self
|
39
|
+
end
|
40
|
+
|
41
|
+
# Set the hue, value: 0 to 255 (red).
|
42
|
+
# See Milight::V6::Color for predefined colors.
|
43
|
+
def hue(value)
|
44
|
+
raise ArgumentError, "Please supply a hue value between 0-255." if value.negative? || value > 255
|
45
|
+
|
46
|
+
@command.execute(1, [0x31, 0x00, 0x00, 0x00, 0x01, value, value, value, value])
|
47
|
+
|
48
|
+
self
|
49
|
+
end
|
50
|
+
|
51
|
+
# Wait before continuing to next command.
|
52
|
+
def wait(seconds)
|
53
|
+
sleep(seconds)
|
54
|
+
|
55
|
+
self
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
data/lib/milight/v6/command.rb
CHANGED
@@ -7,59 +7,19 @@ module Milight
|
|
7
7
|
module V6
|
8
8
|
# see https://github.com/Fantasmos/LimitlessLED-DevAPI
|
9
9
|
class Command
|
10
|
-
|
11
|
-
@socket = Milight::V6::Socket.new(host, port)
|
10
|
+
attr_accessor :wait
|
12
11
|
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
execute(zone_id, [0x3D, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00])
|
18
|
-
end
|
19
|
-
|
20
|
-
def unlink(zone_id)
|
21
|
-
execute(zone_id, [0x3E, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00])
|
22
|
-
end
|
23
|
-
|
24
|
-
def on(zone_id)
|
25
|
-
execute(zone_id, [0x31, 0x00, 0x00, 0x08, 0x04, 0x01, 0x00, 0x00, 0x00])
|
26
|
-
end
|
27
|
-
|
28
|
-
def off(zone_id)
|
29
|
-
execute(zone_id, [0x31, 0x00, 0x00, 0x08, 0x04, 0x02, 0x00, 0x00, 0x00])
|
30
|
-
end
|
31
|
-
|
32
|
-
def night_light(zone_id)
|
33
|
-
execute(zone_id, [0x31, 0x00, 0x00, 0x08, 0x04, 0x05, 0x00, 0x00, 0x00])
|
34
|
-
end
|
35
|
-
|
36
|
-
def brightness(zone_id, value)
|
37
|
-
raise ArgumentError, "Please supply a brightness value between 0-100." if value.negative? || value > 100
|
38
|
-
|
39
|
-
execute(zone_id, [0x31, 0x00, 0x00, 0x08, 0x03, value, 0x00, 0x00, 0x00])
|
40
|
-
end
|
41
|
-
|
42
|
-
def temperature(zone_id, value)
|
43
|
-
raise ArgumentError, "Please supply a temperature value between 0-100 (2700K to 6500K)." if value.negative? || value > 100
|
44
|
-
|
45
|
-
execute(zone_id, [0x31, 0x00, 0x00, 0x08, 0x05, value, 0x00, 0x00, 0x00])
|
46
|
-
end
|
47
|
-
|
48
|
-
def hue(zone_id, value)
|
49
|
-
raise ArgumentError, "Please supply a hue value between 0-255." if value.negative? || value > 255
|
50
|
-
|
51
|
-
execute(zone_id, [0x31, 0x00, 0x00, 0x08, 0x01, value, value, value, value])
|
52
|
-
end
|
53
|
-
|
54
|
-
def saturation(zone_id, value)
|
55
|
-
raise ArgumentError, "Please supply a saturation value between 0-100." if value.negative? || value > 100
|
56
|
-
|
57
|
-
execute(zone_id, [0x31, 0x00, 0x00, 0x08, 0x02, value, 0x00, 0x00, 0x00])
|
12
|
+
def initialize(socket, wait:)
|
13
|
+
@socket = socket
|
14
|
+
@wait = wait
|
15
|
+
@last_time = 0
|
58
16
|
end
|
59
17
|
|
60
18
|
def execute(zone_id, command)
|
61
19
|
raise ArgumentError, "Please supply a zone ID between 1-4." if zone_id.negative? || zone_id > 4
|
62
20
|
|
21
|
+
bridge_session
|
22
|
+
|
63
23
|
# UDP Hex Send Format: 80 00 00 00 11 {WifiBridgeSessionID1} {WifiBridgeSessionID2} 00 {SequenceNumber} 00 {COMMAND} {ZONE NUMBER} 00 {Checksum}
|
64
24
|
request = [0x80, 0x00, 0x00, 0x00, 0x11, @session_id1, @session_id2, 0x00, next_sequence_number, 0x00]
|
65
25
|
|
@@ -69,22 +29,25 @@ module Milight
|
|
69
29
|
request << 0x00
|
70
30
|
request << calculate_checksum(request)
|
71
31
|
|
72
|
-
|
73
|
-
@socket.receive_bytes
|
32
|
+
send_delayed(request)
|
74
33
|
end
|
75
34
|
|
76
35
|
private
|
77
36
|
|
78
37
|
def bridge_session
|
38
|
+
return if !@session_id1.nil? || !@session_id2.nil?
|
39
|
+
|
79
40
|
# UDP.SEND hex bytes: 20 00 00 00 16 02 62 3A D5 ED A3 01 AE 08 2D 46 61 41 A7 F6 DC AF (D3 E6) 00 00 1E <-- Send this to the ip address of the wifi bridge v6
|
80
41
|
request = [0x20, 0x00, 0x00, 0x00, 0x16, 0x02, 0x62, 0x3A, 0xD5,
|
81
42
|
0xED, 0xA3, 0x01, 0xAE, 0x08, 0x2D, 0x46, 0x61, 0x41,
|
82
43
|
0xA7, 0xF6, 0xDC, 0xAF, 0xD3, 0xE6, 0x00, 0x00, 0x1E]
|
83
44
|
|
84
45
|
@socket.send_bytes(request)
|
85
|
-
response = @socket.receive_bytes
|
46
|
+
response, _address = @socket.receive_bytes
|
86
47
|
|
87
|
-
raise Exception, "Could not establish session with Wifi bridge."
|
48
|
+
raise Exception, "Could not establish session with Wifi bridge." if response.nil?
|
49
|
+
|
50
|
+
record_last_time
|
88
51
|
|
89
52
|
@session_id1 = response[19]
|
90
53
|
@session_id2 = response[20]
|
@@ -106,6 +69,33 @@ module Milight
|
|
106
69
|
|
107
70
|
checksum & 0xFF
|
108
71
|
end
|
72
|
+
|
73
|
+
# Delay execution of commands to prevent commands being dropped by the controller.
|
74
|
+
def send_delayed(request)
|
75
|
+
delay_command
|
76
|
+
|
77
|
+
@socket.send_bytes(request)
|
78
|
+
@socket.receive_bytes
|
79
|
+
|
80
|
+
record_last_time
|
81
|
+
end
|
82
|
+
|
83
|
+
def delay_command
|
84
|
+
return unless @wait
|
85
|
+
|
86
|
+
interval = current_time - @last_time
|
87
|
+
sleep(@wait - interval) if interval < @wait
|
88
|
+
end
|
89
|
+
|
90
|
+
def record_last_time
|
91
|
+
return unless @wait
|
92
|
+
|
93
|
+
@last_time = current_time
|
94
|
+
end
|
95
|
+
|
96
|
+
def current_time
|
97
|
+
Process.clock_gettime(Process::CLOCK_MONOTONIC)
|
98
|
+
end
|
109
99
|
end
|
110
100
|
end
|
111
101
|
end
|
@@ -1,14 +1,20 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require "milight/v6/command"
|
4
|
+
require "milight/v6/discover"
|
4
5
|
require "milight/v6/all"
|
6
|
+
require "milight/v6/bridge"
|
5
7
|
require "milight/v6/zone"
|
6
8
|
|
7
9
|
module Milight
|
8
10
|
module V6
|
11
|
+
# Controller for the Mi-Light WiFi iBox.
|
9
12
|
class Controller
|
10
|
-
|
11
|
-
|
13
|
+
extend Milight::V6::Discover
|
14
|
+
|
15
|
+
def initialize(host = "<broadcast>", port = 5987, wait: 0.1)
|
16
|
+
@socket = Milight::V6::Socket.new(host, port)
|
17
|
+
@command = Milight::V6::Command.new(@socket, wait: wait)
|
12
18
|
end
|
13
19
|
|
14
20
|
# Select all zones.
|
@@ -20,6 +26,15 @@ module Milight
|
|
20
26
|
def zone(zone_id)
|
21
27
|
Milight::V6::Zone.new(@command, zone_id)
|
22
28
|
end
|
29
|
+
|
30
|
+
# Select the bridge lamp.
|
31
|
+
def bridge
|
32
|
+
Milight::V6::Bridge.new(@command)
|
33
|
+
end
|
34
|
+
|
35
|
+
def to_s
|
36
|
+
"Mi-Light Wifi iBox Controller. IP address: #{@socket.host}"
|
37
|
+
end
|
23
38
|
end
|
24
39
|
end
|
25
40
|
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "milight/v6/controller"
|
4
|
+
require "milight/v6/socket"
|
5
|
+
|
6
|
+
module Milight
|
7
|
+
module V6
|
8
|
+
# Search for Mi-Light devices.
|
9
|
+
module Discover
|
10
|
+
AUTH_TOKEN = [0x39, 0x38, 0x35, 0x62, 0x31, 0x35, 0x37, 0x62, 0x66, 0x36, 0x66,
|
11
|
+
0x63, 0x34, 0x33, 0x33, 0x36, 0x38, 0x61, 0x36, 0x33, 0x34, 0x36,
|
12
|
+
0x37, 0x65, 0x61, 0x33, 0x62, 0x31, 0x39, 0x64, 0x30, 0x64].freeze
|
13
|
+
|
14
|
+
def search
|
15
|
+
socket = Milight::V6::Socket.new("<broadcast>", 5987)
|
16
|
+
|
17
|
+
bytes = [0x10, 0x00, 0x00, 0x00, 0x24, 0x02, 0xEE, 0x3E, 0x02] + AUTH_TOKEN
|
18
|
+
socket.send_bytes(bytes)
|
19
|
+
|
20
|
+
controllers = []
|
21
|
+
|
22
|
+
loop do
|
23
|
+
bytes, address = socket.receive_bytes
|
24
|
+
break if bytes.nil?
|
25
|
+
|
26
|
+
token = bytes[14, AUTH_TOKEN.length]
|
27
|
+
controllers << Milight::V6::Controller.new(address) if token == AUTH_TOKEN
|
28
|
+
end
|
29
|
+
|
30
|
+
socket.close
|
31
|
+
|
32
|
+
controllers
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
data/lib/milight/v6/socket.rb
CHANGED
@@ -5,41 +5,65 @@ require "socket"
|
|
5
5
|
|
6
6
|
module Milight
|
7
7
|
module V6
|
8
|
+
# Send and receive UDP packets.
|
8
9
|
class Socket
|
9
10
|
READ_TIMEOUT = 5
|
10
11
|
|
11
|
-
|
12
|
-
@socket = UDPSocket.new
|
13
|
-
@socket.connect(host, port)
|
12
|
+
attr_reader :host, :port
|
14
13
|
|
15
|
-
|
16
|
-
@
|
14
|
+
def initialize(host, port)
|
15
|
+
@host = host
|
16
|
+
@port = port
|
17
17
|
end
|
18
18
|
|
19
19
|
def send_bytes(bytes)
|
20
|
-
|
20
|
+
logger.debug("Sending: #{format_bytes_as_hex(bytes)}")
|
21
21
|
|
22
|
-
|
22
|
+
socket.send(bytes.pack('C*'), 0, @host, @port)
|
23
23
|
end
|
24
24
|
|
25
25
|
def receive_bytes
|
26
|
-
response =
|
26
|
+
response, address = socket.recvfrom_nonblock(128)
|
27
27
|
bytes = response.unpack('C*')
|
28
28
|
|
29
|
-
|
29
|
+
logger.debug("Received: #{format_bytes_as_hex(bytes)}")
|
30
30
|
|
31
|
-
bytes
|
31
|
+
[bytes, address.last]
|
32
32
|
rescue IO::WaitReadable
|
33
|
-
ready = IO.select([
|
33
|
+
ready = IO.select([socket], nil, nil, READ_TIMEOUT)
|
34
34
|
retry if ready
|
35
35
|
|
36
|
-
return
|
36
|
+
return nil
|
37
|
+
end
|
38
|
+
|
39
|
+
def close
|
40
|
+
socket.close
|
37
41
|
end
|
38
42
|
|
39
43
|
private
|
40
44
|
|
45
|
+
def socket
|
46
|
+
@socket ||= begin
|
47
|
+
socket = UDPSocket.new
|
48
|
+
|
49
|
+
if @host == "<broadcast>" || @host == "255.255.255.255"
|
50
|
+
socket.setsockopt(::Socket::SOL_SOCKET, ::Socket::SO_BROADCAST, true)
|
51
|
+
end
|
52
|
+
|
53
|
+
socket
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def logger
|
58
|
+
@logger ||= begin
|
59
|
+
logger = Logger.new(STDOUT)
|
60
|
+
logger.level = Logger::INFO if ENV["MILIGHT_DEBUG"] != "1"
|
61
|
+
logger
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
41
65
|
def format_bytes_as_hex(bytes)
|
42
|
-
bytes.map { |s| format("0x%02X", s) }
|
66
|
+
bytes.map { |s| format("0x%02X", s) }.join(", ")
|
43
67
|
end
|
44
68
|
end
|
45
69
|
end
|
data/lib/milight/v6/version.rb
CHANGED
data/lib/milight/v6/zone.rb
CHANGED
@@ -2,6 +2,7 @@
|
|
2
2
|
|
3
3
|
module Milight
|
4
4
|
module V6
|
5
|
+
# Commands for lamps in a specific zone.
|
5
6
|
class Zone
|
6
7
|
attr_reader :zone_id
|
7
8
|
|
@@ -12,78 +13,82 @@ module Milight
|
|
12
13
|
|
13
14
|
# Link/sync light bulbs.
|
14
15
|
def link
|
15
|
-
@command.
|
16
|
+
@command.execute(zone_id, [0x3D, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00])
|
16
17
|
|
17
18
|
self
|
18
19
|
end
|
19
20
|
|
20
21
|
# Unlink/clear light bulbs.
|
21
22
|
def unlink
|
22
|
-
@command.
|
23
|
+
@command.execute(zone_id, [0x3E, 0x00, 0x00, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00])
|
23
24
|
|
24
25
|
self
|
25
26
|
end
|
26
27
|
|
27
28
|
# Switch the lights on.
|
28
29
|
def on
|
29
|
-
@command.
|
30
|
+
@command.execute(zone_id, [0x31, 0x00, 0x00, 0x08, 0x04, 0x01, 0x00, 0x00, 0x00])
|
30
31
|
|
31
32
|
self
|
32
33
|
end
|
33
34
|
|
34
35
|
# Switch the lights off.
|
35
36
|
def off
|
36
|
-
@command.
|
37
|
+
@command.execute(zone_id, [0x31, 0x00, 0x00, 0x08, 0x04, 0x02, 0x00, 0x00, 0x00])
|
37
38
|
|
38
39
|
self
|
39
40
|
end
|
40
41
|
|
41
42
|
# Enable night light mode.
|
42
43
|
def night_light
|
43
|
-
@command.
|
44
|
+
@command.execute(zone_id, [0x31, 0x00, 0x00, 0x08, 0x04, 0x05, 0x00, 0x00, 0x00])
|
44
45
|
|
45
46
|
self
|
46
47
|
end
|
47
48
|
|
48
|
-
# Set brightness, value: 0% to 100
|
49
|
+
# Set brightness, value: 0% to 100%.
|
49
50
|
def brightness(value)
|
50
|
-
|
51
|
+
raise ArgumentError, "Please supply a brightness value between 0-100." if value.negative? || value > 100
|
52
|
+
|
53
|
+
@command.execute(zone_id, [0x31, 0x00, 0x00, 0x08, 0x03, value, 0x00, 0x00, 0x00])
|
51
54
|
|
52
55
|
self
|
53
56
|
end
|
54
57
|
|
55
58
|
# Set color temperature, value: 0 = 2700K, 100 = 6500K.
|
56
59
|
def temperature(value)
|
57
|
-
|
60
|
+
raise ArgumentError, "Please supply a temperature value between 0-100 (2700K to 6500K)." if value.negative? || value > 100
|
61
|
+
|
62
|
+
@command.execute(zone_id, [0x31, 0x00, 0x00, 0x08, 0x05, value, 0x00, 0x00, 0x00])
|
58
63
|
|
59
64
|
self
|
60
65
|
end
|
61
66
|
|
62
67
|
# Set color temperature to warm light (2700K).
|
63
68
|
def warm_light
|
64
|
-
|
65
|
-
|
66
|
-
self
|
69
|
+
temperature(0)
|
67
70
|
end
|
68
71
|
|
69
72
|
# Set color temperature to white (cool) light (6500K).
|
70
73
|
def white_light
|
71
|
-
|
72
|
-
|
73
|
-
self
|
74
|
+
temperature(100)
|
74
75
|
end
|
75
76
|
|
76
77
|
# Set the hue, value: 0 to 255 (red).
|
77
78
|
# See Milight::V6::Color for predefined colors.
|
78
79
|
def hue(value)
|
79
|
-
|
80
|
+
raise ArgumentError, "Please supply a hue value between 0-255." if value.negative? || value > 255
|
81
|
+
|
82
|
+
@command.execute(zone_id, [0x31, 0x00, 0x00, 0x08, 0x01, value, value, value, value])
|
80
83
|
|
81
84
|
self
|
82
85
|
end
|
83
86
|
|
84
87
|
# Set the saturation, value: 0% to 100%.
|
85
88
|
def saturation(value)
|
86
|
-
|
89
|
+
raise ArgumentError, "Please supply a saturation value between 0-100." if value.negative? || value > 100
|
90
|
+
|
91
|
+
@command.execute(zone_id, [0x31, 0x00, 0x00, 0x08, 0x02, value, 0x00, 0x00, 0x00])
|
87
92
|
|
88
93
|
self
|
89
94
|
end
|
data/milight-v6.gemspec
CHANGED
@@ -21,7 +21,7 @@ Gem::Specification.new do |spec|
|
|
21
21
|
spec.executables = ["milight"]
|
22
22
|
spec.require_paths = ["lib"]
|
23
23
|
|
24
|
-
spec.add_development_dependency "bundler", "~>
|
25
|
-
spec.add_development_dependency "rake", "~>
|
24
|
+
spec.add_development_dependency "bundler", "~> 2.0"
|
25
|
+
spec.add_development_dependency "rake", "~> 13.0"
|
26
26
|
spec.add_development_dependency "rspec", "~> 3.0"
|
27
27
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: milight-v6
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Peter Postma
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2021-04-28 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -16,28 +16,28 @@ dependencies:
|
|
16
16
|
requirements:
|
17
17
|
- - "~>"
|
18
18
|
- !ruby/object:Gem::Version
|
19
|
-
version: '
|
19
|
+
version: '2.0'
|
20
20
|
type: :development
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
24
|
- - "~>"
|
25
25
|
- !ruby/object:Gem::Version
|
26
|
-
version: '
|
26
|
+
version: '2.0'
|
27
27
|
- !ruby/object:Gem::Dependency
|
28
28
|
name: rake
|
29
29
|
requirement: !ruby/object:Gem::Requirement
|
30
30
|
requirements:
|
31
31
|
- - "~>"
|
32
32
|
- !ruby/object:Gem::Version
|
33
|
-
version: '
|
33
|
+
version: '13.0'
|
34
34
|
type: :development
|
35
35
|
prerelease: false
|
36
36
|
version_requirements: !ruby/object:Gem::Requirement
|
37
37
|
requirements:
|
38
38
|
- - "~>"
|
39
39
|
- !ruby/object:Gem::Version
|
40
|
-
version: '
|
40
|
+
version: '13.0'
|
41
41
|
- !ruby/object:Gem::Dependency
|
42
42
|
name: rspec
|
43
43
|
requirement: !ruby/object:Gem::Requirement
|
@@ -75,9 +75,11 @@ files:
|
|
75
75
|
- bin/setup
|
76
76
|
- lib/milight/v6.rb
|
77
77
|
- lib/milight/v6/all.rb
|
78
|
+
- lib/milight/v6/bridge.rb
|
78
79
|
- lib/milight/v6/color.rb
|
79
80
|
- lib/milight/v6/command.rb
|
80
81
|
- lib/milight/v6/controller.rb
|
82
|
+
- lib/milight/v6/discover.rb
|
81
83
|
- lib/milight/v6/exception.rb
|
82
84
|
- lib/milight/v6/socket.rb
|
83
85
|
- lib/milight/v6/version.rb
|
@@ -87,7 +89,7 @@ homepage: https://github.com/ppostma/milight-v6-api
|
|
87
89
|
licenses:
|
88
90
|
- MIT
|
89
91
|
metadata: {}
|
90
|
-
post_install_message:
|
92
|
+
post_install_message:
|
91
93
|
rdoc_options: []
|
92
94
|
require_paths:
|
93
95
|
- lib
|
@@ -102,9 +104,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
102
104
|
- !ruby/object:Gem::Version
|
103
105
|
version: '0'
|
104
106
|
requirements: []
|
105
|
-
|
106
|
-
|
107
|
-
signing_key:
|
107
|
+
rubygems_version: 3.1.4
|
108
|
+
signing_key:
|
108
109
|
specification_version: 4
|
109
110
|
summary: Ruby API for the Milight Wifi Bridge v6
|
110
111
|
test_files: []
|