wizrb 1.1.1 → 1.1.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +5 -0
  3. data/LICENSE.txt +21 -0
  4. data/README.md +62 -0
  5. data/doc/devices.md +135 -0
  6. data/doc/lighting.md +186 -0
  7. data/doc/power.md +37 -0
  8. data/exe/wizrb +5 -0
  9. data/lib/wizrb/cli.rb +79 -0
  10. data/lib/wizrb/lighting/discover.rb +40 -0
  11. data/lib/wizrb/lighting/events/set_brightness_event.rb +27 -0
  12. data/lib/wizrb/lighting/events/set_cold_white_event.rb +27 -0
  13. data/lib/wizrb/lighting/events/set_color_temp_event.rb +29 -0
  14. data/lib/wizrb/lighting/events/set_rgb_event.rb +29 -0
  15. data/lib/wizrb/lighting/events/set_scene_event.rb +22 -0
  16. data/lib/wizrb/lighting/events/set_speed_event.rb +27 -0
  17. data/lib/wizrb/lighting/events/set_warm_white_event.rb +27 -0
  18. data/lib/wizrb/lighting/group.rb +37 -0
  19. data/lib/wizrb/lighting/products/dimable_light.rb +33 -0
  20. data/lib/wizrb/lighting/products/light.rb +55 -0
  21. data/lib/wizrb/lighting/products/rgb_light.rb +58 -0
  22. data/lib/wizrb/lighting/products/tunable_light.rb +39 -0
  23. data/lib/wizrb/lighting/scenes/scene.rb +68 -0
  24. data/lib/wizrb/lighting/scenes/spooky_scene.rb +61 -0
  25. data/lib/wizrb/lighting/state.rb +45 -0
  26. data/lib/wizrb/lighting.rb +41 -0
  27. data/lib/wizrb/power/discover.rb +34 -0
  28. data/lib/wizrb/power/group.rb +10 -0
  29. data/lib/wizrb/power/products/smart_plug.rb +17 -0
  30. data/lib/wizrb/shared/connection.rb +76 -0
  31. data/lib/wizrb/shared/discover.rb +116 -0
  32. data/lib/wizrb/shared/events/base.rb +22 -0
  33. data/lib/wizrb/shared/events/power_event.rb +22 -0
  34. data/lib/wizrb/shared/events/reboot_event.rb +15 -0
  35. data/lib/wizrb/shared/events/refresh_event.rb +15 -0
  36. data/lib/wizrb/shared/events/reset_event.rb +15 -0
  37. data/lib/wizrb/shared/group.rb +68 -0
  38. data/lib/wizrb/shared/products/device.rb +105 -0
  39. data/lib/wizrb/shared/state.rb +32 -0
  40. data/lib/wizrb/version.rb +5 -0
  41. data/lib/wizrb.rb +32 -0
  42. metadata +45 -4
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../shared/events/base'
4
+
5
+ module Wizrb
6
+ module Lighting
7
+ module Events
8
+ class SetRgbEvent < Wizrb::Shared::Events::Base
9
+ MIN_VALUE = 1
10
+ MAX_VALUE = 255
11
+
12
+ def initialize(red, green, blue)
13
+ validate_color!('Red', red)
14
+ validate_color!('Green', green)
15
+ validate_color!('Blue', blue)
16
+ super(method: 'setState', params: { r: red, g: green, b: blue })
17
+ end
18
+
19
+ private
20
+
21
+ def validate_color!(color, value)
22
+ return if value && value >= MIN_VALUE && value <= MAX_VALUE
23
+
24
+ raise ArgumentError, "#{color} must be between #{MIN_VALUE} and #{MAX_VALUE}"
25
+ end
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../shared/events/base'
4
+
5
+ module Wizrb
6
+ module Lighting
7
+ module Events
8
+ class SetSceneEvent < Wizrb::Shared::Events::Base
9
+ def initialize(value)
10
+ validate!(value)
11
+ super(method: 'setState', params: { sceneId: Wizrb::Lighting::SCENES[value] })
12
+ end
13
+
14
+ private
15
+
16
+ def validate!(value)
17
+ raise ArgumentError, 'Invalid scene' unless Wizrb::Lighting::SCENES.include?(value)
18
+ end
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../shared/events/base'
4
+
5
+ module Wizrb
6
+ module Lighting
7
+ module Events
8
+ class SetSpeedEvent < Wizrb::Shared::Events::Base
9
+ MIN_VALUE = 10
10
+ MAX_VALUE = 200
11
+
12
+ def initialize(value)
13
+ validate!(value)
14
+ super(method: 'setState', params: { speed: value })
15
+ end
16
+
17
+ private
18
+
19
+ def validate!(value)
20
+ return if value && value >= MIN_VALUE && value <= MAX_VALUE
21
+
22
+ raise ArgumentError, "Speed must be between #{MIN_VALUE} and #{MAX_VALUE}"
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../shared/events/base'
4
+
5
+ module Wizrb
6
+ module Lighting
7
+ module Events
8
+ class SetWarmWhiteEvent < Wizrb::Shared::Events::Base
9
+ MIN_VALUE = 1
10
+ MAX_VALUE = 255
11
+
12
+ def initialize(value)
13
+ validate!(value)
14
+ super(method: 'setState', params: { w: value })
15
+ end
16
+
17
+ private
18
+
19
+ def validate!(value)
20
+ return if value && value >= MIN_VALUE && value <= MAX_VALUE
21
+
22
+ raise ArgumentError, "Warm white must be between #{MIN_VALUE} and #{MAX_VALUE}"
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../shared/group'
4
+
5
+ module Wizrb
6
+ module Lighting
7
+ class Group < Wizrb::Shared::Group
8
+ def brightness(value)
9
+ dispatch_event(Wizrb::Lighting::Events::SetBrightnessEvent.new(value))
10
+ end
11
+
12
+ def cold_white(value)
13
+ dispatch_event(Wizrb::Lighting::Events::SetColdWhiteEvent.new(value))
14
+ end
15
+
16
+ def color_temp(value)
17
+ dispatch_event(Wizrb::Lighting::Events::SetColorTempEvent.new(value))
18
+ end
19
+
20
+ def rgb(red, green, blue)
21
+ dispatch_event(Wizrb::Lighting::Events::SetRgbEvent.new(red, green, blue))
22
+ end
23
+
24
+ def speed(value)
25
+ dispatch_event(Wizrb::Lighting::Events::SetSpeedEvent.new(value))
26
+ end
27
+
28
+ def warm_white(value)
29
+ dispatch_event(Wizrb::Lighting::Events::SetWarmWhiteEvent.new(value))
30
+ end
31
+
32
+ def scene(value)
33
+ dispatch_event(Wizrb::Lighting::Events::SetSceneEvent.new(value))
34
+ end
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'light'
4
+
5
+ module Wizrb
6
+ module Lighting
7
+ module Products
8
+ class DimableLight < Wizrb::Lighting::Products::Light
9
+ MODULE_NAME_IDENTIFIER = 'DW'
10
+ FEATURES = {
11
+ brightness: true,
12
+ color_temp: false,
13
+ color: false,
14
+ effect: false,
15
+ scenes: %i[
16
+ wake_up
17
+ bedtime
18
+ cool_white
19
+ night_light
20
+ candlelight
21
+ golden_white
22
+ pulse
23
+ steampunk
24
+ ]
25
+ }.freeze
26
+
27
+ def initialize(ip:, port: 38_899)
28
+ super
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'socket'
5
+ require_relative '../../shared/products/device'
6
+ require_relative '../state'
7
+
8
+ module Wizrb
9
+ module Lighting
10
+ module Products
11
+ class Light < Wizrb::Shared::Products::Device
12
+ def initialize(ip:, port: 38_899)
13
+ super(ip: ip, port: port, state: Wizrb::Lighting::State.new)
14
+ end
15
+
16
+ def white_range
17
+ @white_range ||=
18
+ user_config&.dig('whiteRange') ||
19
+ model_config&.dig('extRange') ||
20
+ model_config&.dig('cctRange') ||
21
+ user_config&.dig('extRange') ||
22
+ user_config&.dig('cctRange')
23
+ end
24
+
25
+ def brightness(value)
26
+ dispatch_event(Wizrb::Lighting::Events::SetBrightnessEvent.new(value))
27
+ end
28
+
29
+ def cold_white(value)
30
+ dispatch_event(Wizrb::Lighting::Events::SetColdWhiteEvent.new(value))
31
+ end
32
+
33
+ def color_temp(value)
34
+ dispatch_event(Wizrb::Lighting::Events::SetColorTempEvent.new(value))
35
+ end
36
+
37
+ def rgb(red, green, blue)
38
+ dispatch_event(Wizrb::Lighting::Events::SetRgbEvent.new(red, green, blue))
39
+ end
40
+
41
+ def speed(value)
42
+ dispatch_event(Wizrb::Lighting::Events::SetSpeedEvent.new(value))
43
+ end
44
+
45
+ def warm_white(value)
46
+ dispatch_event(Wizrb::Lighting::Events::SetWarmWhiteEvent.new(value))
47
+ end
48
+
49
+ def scene(value)
50
+ dispatch_event(Wizrb::Lighting::Events::SetSceneEvent.new(value))
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'light'
4
+
5
+ module Wizrb
6
+ module Lighting
7
+ module Products
8
+ class RgbLight < Wizrb::Lighting::Products::Light
9
+ MODULE_NAME_IDENTIFIER = 'RGB'
10
+ FEATURES = {
11
+ brightness: true,
12
+ color_temp: true,
13
+ color: true,
14
+ effect: true,
15
+ scenes: %i[
16
+ ocean
17
+ romance
18
+ sunset
19
+ party
20
+ fireplace
21
+ cozy
22
+ forest
23
+ pastel_colors
24
+ wake_up
25
+ bedtime
26
+ warm_white
27
+ daylight
28
+ cool_white
29
+ night_light
30
+ focus
31
+ relax
32
+ true_colors
33
+ tv_time
34
+ plantgrowth
35
+ spring
36
+ summer
37
+ fall
38
+ deepdive
39
+ jungle
40
+ mojito
41
+ club
42
+ christmas
43
+ halloween
44
+ candlelight
45
+ golden_white
46
+ pulse
47
+ steampunk
48
+ rhythm
49
+ ]
50
+ }.freeze
51
+
52
+ def initialize(ip:, port: 38_899)
53
+ super
54
+ end
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'light'
4
+
5
+ module Wizrb
6
+ module Lighting
7
+ module Products
8
+ class TunableLight < Wizrb::Lighting::Products::Light
9
+ MODULE_NAME_IDENTIFIER = 'TW'
10
+ FEATURES = {
11
+ brightness: true,
12
+ color_temp: true,
13
+ color: false,
14
+ effect: true,
15
+ scenes: %i[
16
+ cozy
17
+ wake_up
18
+ bedtime
19
+ warm_white
20
+ daylight
21
+ cool_white
22
+ night_light
23
+ focus
24
+ relax
25
+ tv_time
26
+ candlelight
27
+ golden_white
28
+ pulse
29
+ steampunk
30
+ ]
31
+ }.freeze
32
+
33
+ def initialize(ip:, port: 38_899)
34
+ super
35
+ end
36
+ end
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../group'
4
+
5
+ module Wizrb
6
+ module Lighting
7
+ module Scenes
8
+ class Scene
9
+ def initialize(group, stop_delay: 1, save: true)
10
+ validate!(group)
11
+ @group = group
12
+ @stop_delay = stop_delay
13
+ @save = save
14
+ @running = false
15
+ @thread = nil
16
+ end
17
+
18
+ def start
19
+ @running = true
20
+ @thread = Thread.new do
21
+ save_state if @save
22
+ before_start
23
+ step while @running
24
+ after_stop
25
+ ensure
26
+ restore_state if @save
27
+ end
28
+ end
29
+
30
+ def stop
31
+ @running = false
32
+ sleep(@stop_delay)
33
+ @thread.terminate
34
+ end
35
+
36
+ protected
37
+
38
+ def before_start; end
39
+
40
+ def step
41
+ raise Wizrb::Error, 'You failed to implement #step on your scene'
42
+ end
43
+
44
+ def after_stop; end
45
+
46
+ private
47
+
48
+ def validate!(value)
49
+ return if value.is_a?(Wizrb::Lighting::Group)
50
+
51
+ raise Wizrb::Error, 'Scenes can only be activated on lighting groups'
52
+ end
53
+
54
+ def save_state
55
+ @state_events = @group.map do |device|
56
+ Wizrb::Shared::Events::Base.new(method: 'setState', params: device.refresh.state)
57
+ end
58
+ end
59
+
60
+ def restore_state
61
+ @group.each_with_index do |device, i|
62
+ device.dispatch_event(@state_events[i])
63
+ end
64
+ end
65
+ end
66
+ end
67
+ end
68
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'scene'
4
+
5
+ module Wizrb
6
+ module Lighting
7
+ module Scenes
8
+ class SpookyScene < Wizrb::Lighting::Scenes::Scene
9
+ DEFAULT_STATE_EVENTS = [
10
+ Wizrb::Shared::Events::PowerEvent.new(true),
11
+ Wizrb::Lighting::Events::SetSpeedEvent.new(200),
12
+ Wizrb::Lighting::Events::SetColorTempEvent.new(1000),
13
+ Wizrb::Lighting::Events::SetBrightnessEvent.new(100)
14
+ ].freeze
15
+
16
+ BLACKOUT_EVENTS = [
17
+ Wizrb::Shared::Events::PowerEvent.new(false)
18
+ ].freeze
19
+
20
+ def initialize(group, sync: false)
21
+ @sync = sync
22
+ super(group, stop_delay: 10)
23
+ end
24
+
25
+ protected
26
+
27
+ def before_start
28
+ dispatch(DEFAULT_STATE_EVENTS, sync: true)
29
+ end
30
+
31
+ def step
32
+ if Random.rand > 0.5
33
+ Random.rand(1..10).times do
34
+ next if Random.rand > 0.25
35
+
36
+ dispatch(BLACKOUT_EVENTS, sync: @sync)
37
+ sleep(Random.rand > 0.98 ? 3 : 0.125)
38
+ dispatch(DEFAULT_STATE_EVENTS, sync: true)
39
+ end
40
+ end
41
+
42
+ sleep(Random.rand(0.125..5.0))
43
+ end
44
+
45
+ def after_stop
46
+ dispatch(DEFAULT_STATE_EVENTS, sync: true)
47
+ end
48
+
49
+ private
50
+
51
+ def dispatch(events, sync: false)
52
+ if sync || Random.rand > 0.5
53
+ @group.dispatch_events(*events)
54
+ else
55
+ @group.devices.sample.dispatch_events(*events)
56
+ end
57
+ end
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Wizrb
4
+ module Lighting
5
+ class State < Wizrb::Shared::State
6
+ STATE_KEYS = %i[state w c r g b temp dimming speed sceneId].freeze
7
+
8
+ def parse!(response)
9
+ result = response&.dig('result')
10
+ return unless result
11
+
12
+ @state = result.transform_keys(&:to_sym).slice(*STATE_KEYS)
13
+ @state.delete(:sceneId) if @state[:sceneId]&.zero?
14
+ end
15
+
16
+ def warm_white
17
+ @state[:w]
18
+ end
19
+
20
+ def cold_white
21
+ @state[:c]
22
+ end
23
+
24
+ def rgb
25
+ { red: @state[:r], green: @state[:g], blue: @state[:b] }
26
+ end
27
+
28
+ def color_temp
29
+ @state[:temp]
30
+ end
31
+
32
+ def brightness
33
+ @state[:dimming]
34
+ end
35
+
36
+ def speed
37
+ @state[:speed]
38
+ end
39
+
40
+ def scene
41
+ Wizrb::Lighting::SCENES.key(@state[:schdPsetId] || @state[:sceneId])
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Wizrb
4
+ module Lighting
5
+ SCENES = {
6
+ ocean: 1,
7
+ romance: 2,
8
+ sunset: 3,
9
+ party: 4,
10
+ fireplace: 5,
11
+ cozy: 6,
12
+ forest: 7,
13
+ pastel_colors: 8,
14
+ wake_up: 9,
15
+ bedtime: 10,
16
+ warm_white: 11,
17
+ daylight: 12,
18
+ cool_white: 13,
19
+ night_light: 14,
20
+ focus: 15,
21
+ relax: 16,
22
+ true_colors: 17,
23
+ tv_time: 18,
24
+ plantgrowth: 19,
25
+ spring: 20,
26
+ summer: 21,
27
+ fall: 22,
28
+ deepdive: 23,
29
+ jungle: 24,
30
+ mojito: 25,
31
+ club: 26,
32
+ christmas: 27,
33
+ halloween: 28,
34
+ candlelight: 29,
35
+ golden_white: 30,
36
+ pulse: 31,
37
+ steampunk: 32,
38
+ rhythm: 1000
39
+ }.freeze
40
+ end
41
+ end
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../shared/discover'
4
+ require_relative 'group'
5
+ require_relative 'products/smart_plug'
6
+
7
+ module Wizrb
8
+ module Power
9
+ class Discover < Wizrb::Shared::Discover
10
+ private
11
+
12
+ def parse_response(data, addr)
13
+ response = JSON.parse(data)
14
+ return unless response.dig('result', 'success') && addr[1] && addr[2]
15
+
16
+ resolve_device(ip: addr[2], port: addr[1])
17
+ rescue StandardError
18
+ nil
19
+ end
20
+
21
+ def resolve_device(ip:, port: 38_899)
22
+ module_name = Wizrb::Shared::Products::Device.new(ip: ip, port: port).module_name
23
+
24
+ return unless module_name.include?(Wizrb::Power::Products::SmartPlug::MODULE_NAME_IDENTIFIER)
25
+
26
+ Wizrb::Power::Products::SmartPlug.new(ip: ip, port: port)
27
+ end
28
+
29
+ def group_devices
30
+ Wizrb::Power::Group.new(devices: @devices)
31
+ end
32
+ end
33
+ end
34
+ end
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../shared/group'
4
+
5
+ module Wizrb
6
+ module Power
7
+ class Group < Wizrb::Shared::Group
8
+ end
9
+ end
10
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative '../../shared/products/device'
4
+
5
+ module Wizrb
6
+ module Power
7
+ module Products
8
+ class SmartPlug < Wizrb::Shared::Products::Device
9
+ MODULE_NAME_IDENTIFIER = 'SOCKET'
10
+
11
+ def initialize(ip:, port: 38_899)
12
+ super
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'json'
4
+ require 'socket'
5
+
6
+ module Wizrb
7
+ module Shared
8
+ class Connection
9
+ attr_reader :ip, :port, :connection_id, :socket
10
+
11
+ def initialize(ip, port)
12
+ @ip = ip
13
+ @port = port
14
+ @connection_id = Time.now.to_i.to_s(16)
15
+ @socket = UDPSocket.new
16
+ log('Created')
17
+ end
18
+
19
+ def connect
20
+ with_error_logging do
21
+ socket.connect(ip, port)
22
+ log('Connected')
23
+ end
24
+ end
25
+
26
+ def send(data)
27
+ with_error_logging do
28
+ connect
29
+ log("Sending: #{data.to_json}")
30
+ socket.send(data.to_json.encode('UTF-8'), 0)
31
+ end
32
+ end
33
+
34
+ def receive(timeout: 2, max: 1024)
35
+ with_error_logging do
36
+ connect
37
+
38
+ ready = socket.wait_readable(timeout)
39
+ raise Wizrb::ConnectionTimeoutError unless ready
40
+
41
+ data, _addr = socket.recvfrom(max)
42
+ log("Received: #{data}")
43
+ parse_response(data)
44
+ end
45
+ end
46
+
47
+ def test
48
+ with_error_logging do
49
+ send({ method: 'getPilot', params: {} })
50
+ receive
51
+ end
52
+ end
53
+
54
+ private
55
+
56
+ def parse_response(data)
57
+ response = JSON.parse(data)
58
+
59
+ raise Wizrb::ConnectionError, response['error'] if response.key?('error')
60
+
61
+ response
62
+ end
63
+
64
+ def log(message)
65
+ puts "[Wizrb::Connection##{connection_id} #{ip}:#{port}] #{message}" if ENV['DEBUG']
66
+ end
67
+
68
+ def with_error_logging
69
+ yield
70
+ rescue StandardError => e
71
+ log("Error: #{e.message}")
72
+ raise
73
+ end
74
+ end
75
+ end
76
+ end