milight-v6 0.1.1 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
[![Build Status](https://travis-ci.org/ppostma/milight-v6-api.svg?branch=master)](https://travis-ci.org/ppostma/milight-v6-api)
|
5
5
|
[![Code Climate](https://codeclimate.com/github/ppostma/milight-v6-api/badges/gpa.svg)](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: []
|