wizrb 1.1.1 → 1.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.
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 +57 -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 +64 -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 +70 -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 +56 -8
@@ -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,64 @@
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 ||= primary_white_range || secondary_white_range || tertiary_white_range
18
+ end
19
+
20
+ def brightness(value)
21
+ dispatch_event(Wizrb::Lighting::Events::SetBrightnessEvent.new(value))
22
+ end
23
+
24
+ def cold_white(value)
25
+ dispatch_event(Wizrb::Lighting::Events::SetColdWhiteEvent.new(value))
26
+ end
27
+
28
+ def color_temp(value)
29
+ dispatch_event(Wizrb::Lighting::Events::SetColorTempEvent.new(value))
30
+ end
31
+
32
+ def rgb(red, green, blue)
33
+ dispatch_event(Wizrb::Lighting::Events::SetRgbEvent.new(red, green, blue))
34
+ end
35
+
36
+ def speed(value)
37
+ dispatch_event(Wizrb::Lighting::Events::SetSpeedEvent.new(value))
38
+ end
39
+
40
+ def warm_white(value)
41
+ dispatch_event(Wizrb::Lighting::Events::SetWarmWhiteEvent.new(value))
42
+ end
43
+
44
+ def scene(value)
45
+ dispatch_event(Wizrb::Lighting::Events::SetSceneEvent.new(value))
46
+ end
47
+
48
+ private
49
+
50
+ def primary_white_range
51
+ user_config&.dig("whiteRange")
52
+ end
53
+
54
+ def secondary_white_range
55
+ model_config&.dig("extRange") || model_config&.dig("cctRange")
56
+ end
57
+
58
+ def tertiary_white_range
59
+ user_config&.dig("extRange") || user_config&.dig("cctRange")
60
+ end
61
+ end
62
+ end
63
+ end
64
+ 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,70 @@
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
39
+ end
40
+
41
+ def step
42
+ raise Wizrb::Error, "You failed to implement #step on your scene"
43
+ end
44
+
45
+ def after_stop
46
+ end
47
+
48
+ private
49
+
50
+ def validate!(value)
51
+ return if value.is_a?(Wizrb::Lighting::Group)
52
+
53
+ raise Wizrb::Error, "Scenes can only be activated on lighting groups"
54
+ end
55
+
56
+ def save_state
57
+ @state_events = @group.map do |device|
58
+ Wizrb::Shared::Events::Base.new(method: "setState", params: device.refresh.state)
59
+ end
60
+ end
61
+
62
+ def restore_state
63
+ @group.each_with_index do |device, i|
64
+ device.dispatch_event(@state_events[i])
65
+ end
66
+ end
67
+ end
68
+ end
69
+ end
70
+ 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
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 => e
71
+ log("Error: #{e.message}")
72
+ raise
73
+ end
74
+ end
75
+ end
76
+ end