huebot 0.4.0 → 1.0.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.
@@ -0,0 +1,41 @@
1
+ require 'fileutils'
2
+ require 'yaml'
3
+
4
+ module Huebot
5
+ class Config
6
+ def initialize(path = "~/.config/huebot")
7
+ @path = File.expand_path(path)
8
+ @dir = File.dirname(@path)
9
+ @dir_exists = File.exist? @dir
10
+ @config = File.exist?(@path) ? YAML.load_file(@path) : {}
11
+ end
12
+
13
+ def [](attr)
14
+ @config[attr.to_s]
15
+ end
16
+
17
+ def []=(attr, val)
18
+ if val.nil?
19
+ @config.delete(attr.to_s)
20
+ else
21
+ @config[attr.to_s] = val
22
+ end
23
+ write
24
+ end
25
+
26
+ def clear
27
+ @config.clear
28
+ write
29
+ end
30
+
31
+ private
32
+
33
+ def write
34
+ unless @dir_exists
35
+ FileUtils.mkdir_p @dir
36
+ @dir_exists = true
37
+ end
38
+ File.write(@path, YAML.dump(@config))
39
+ end
40
+ end
41
+ end
@@ -1,51 +1,65 @@
1
1
  module Huebot
2
2
  class DeviceMapper
3
- Unmapped = Class.new(StandardError)
3
+ Unmapped = Class.new(Error)
4
4
 
5
- def initialize(client, inputs = [])
6
- all_lights, all_groups = client.lights, client.groups
5
+ def initialize(bridge, inputs = [])
6
+ all_lights, all_groups = bridge.lights, bridge.groups
7
7
 
8
8
  @lights_by_id = all_lights.reduce({}) { |a, l| a[l.id] = l; a }
9
9
  @lights_by_name = all_lights.reduce({}) { |a, l| a[l.name] = l; a }
10
10
  @groups_by_id = all_groups.reduce({}) { |a, g| a[g.id] = g; a }
11
11
  @groups_by_name = all_groups.reduce({}) { |a, g| a[g.name] = g; a }
12
- @devices_by_var = inputs.each_with_index.reduce({}) { |a, (x, idx)|
13
- dev = case x
14
- when LightInput then @lights_by_id[x.val] || @lights_by_name[x.val]
15
- when GroupInput then @groups_by_id[x.val] || @groups_by_name[x.val]
16
- else raise "Invalid input: #{x}"
17
- end || raise(Unmapped, "Could not find #{x.class.name[8..-6].downcase} with id or name '#{x.val}'")
18
- a["$#{idx + 1}"] = dev
19
- a
12
+ @devices_by_var = inputs.each_with_index.each_with_object({}) { |(x, idx), obj|
13
+ obj[idx + 1] =
14
+ case x
15
+ when Light::Input then @lights_by_id[x.val.to_i] || @lights_by_name[x.val]
16
+ when Group::Input then @groups_by_id[x.val.to_i] || @groups_by_name[x.val]
17
+ else raise Error, "Invalid input: #{x}"
18
+ end || raise(Unmapped, "Could not find #{x.class.name[8..-6].downcase} with id or name '#{x.val}'")
20
19
  }
21
20
  @all = @devices_by_var.values
22
21
  end
23
22
 
23
+ def each
24
+ if block_given?
25
+ @all.each { |device| yield device }
26
+ else
27
+ @all.each
28
+ end
29
+ end
30
+
24
31
  def light!(id)
25
- case id
26
- when Integer
27
- @lights_by_id[id]
28
- when String
29
- @lights_by_name[id]
30
- end || (raise Unmapped, "Unmapped light '#{id}'")
32
+ @lights_by_id[id] || @lights_by_name[id] || (raise Unmapped, "Unmapped light '#{id}'")
31
33
  end
32
34
 
33
35
  def group!(id)
34
- case id
35
- when Integer
36
- @groups_by_id[id]
37
- when String
38
- @groups_by_name[id]
39
- end || (raise Unmapped, "Unmapped group '#{id}'")
36
+ @groups_by_id[id] || @groups_by_name[id] || (raise Unmapped, "Unmapped group '#{id}'")
40
37
  end
41
38
 
42
39
  def var!(id)
43
40
  case id
44
- when "$all"
41
+ when :all
45
42
  @all
46
43
  else
47
- @devices_by_var[id]
48
- end || (raise Unmapped, "Unmapped device '#{id}'")
44
+ @devices_by_var[id] || (raise Unmapped, "Unmapped device '#{id}'")
45
+ end
46
+ end
47
+
48
+ def missing_lights(names)
49
+ names - @lights_by_name.keys
50
+ end
51
+
52
+ def missing_groups(names)
53
+ names - @groups_by_name.keys
54
+ end
55
+
56
+ def missing_vars(vars)
57
+ missing = vars - @devices_by_var.keys
58
+ if @all.any?
59
+ missing - [:all]
60
+ else
61
+ missing
62
+ end
49
63
  end
50
64
  end
51
65
  end
@@ -0,0 +1,11 @@
1
+ module Huebot
2
+ module DeviceState
3
+ def set_state(state)
4
+ client.put!(state_change_url, state)
5
+ end
6
+
7
+ def get_state
8
+ client.get!(url).fetch("state")
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,30 @@
1
+ module Huebot
2
+ class Group
3
+ #
4
+ # Struct for specifying a Group input (id or name)
5
+ #
6
+ # @attr val [Integer|String] id or name
7
+ #
8
+ Input = Struct.new(:val)
9
+
10
+ include DeviceState
11
+ attr_reader :client, :id, :name
12
+
13
+ def initialize(client, id, attrs)
14
+ @client = client
15
+ @id = id.to_i
16
+ @name = attrs.fetch("name")
17
+ @attrs = attrs
18
+ end
19
+
20
+ private
21
+
22
+ def state_change_url
23
+ url "/action"
24
+ end
25
+
26
+ def url(path = "")
27
+ "/groups/#{id}#{path}"
28
+ end
29
+ end
30
+ end
@@ -0,0 +1,30 @@
1
+ module Huebot
2
+ class Light
3
+ #
4
+ # Struct for specifying a Light input (id or name)
5
+ #
6
+ # @attr val [Integer|String] id or name
7
+ #
8
+ Input = Struct.new(:val)
9
+
10
+ include DeviceState
11
+ attr_reader :client, :id, :name
12
+
13
+ def initialize(client, id, attrs)
14
+ @client = client
15
+ @id = id.to_i
16
+ @name = attrs.fetch("name")
17
+ @attrs = attrs
18
+ end
19
+
20
+ private
21
+
22
+ def state_change_url
23
+ url "/state"
24
+ end
25
+
26
+ def url(path = "")
27
+ "/lights/#{id}#{path}"
28
+ end
29
+ end
30
+ end
@@ -1,32 +1,82 @@
1
1
  module Huebot
2
2
  class Program
3
- Transition = Struct.new(:wait, :state, :devices)
4
- ParallelTransition = Struct.new(:wait, :children)
3
+ #
4
+ # Struct for storing a program's Intermediate Representation and source filepath.
5
+ #
6
+ # @attr tokens [Hash]
7
+ # @attr filepath [String]
8
+ # @attr api_version [Float] API version
9
+ #
10
+ Src = Struct.new(:tokens, :filepath, :api_version) do
11
+ def default_name
12
+ File.basename(filepath, ".*")
13
+ end
14
+ end
5
15
 
6
- attr_accessor :name
7
- attr_accessor :initial_state
8
- attr_accessor :transitions
9
- attr_accessor :final_state
10
- attr_accessor :loop
11
- attr_accessor :loops
12
- attr_accessor :errors
13
- attr_accessor :warnings
14
-
15
- def initialize
16
- @name = nil
17
- @initial_state = nil
18
- @transitions = []
19
- @final_state = nil
20
- @loop = false
21
- @loops = 0
22
- @errors = []
23
- @warnings = []
16
+ module AST
17
+ Node = Struct.new(:instruction, :children, :errors, :warnings)
18
+
19
+ Transition = Struct.new(:state, :devices, :sleep)
20
+ SerialControl = Struct.new(:loop, :sleep)
21
+ ParallelControl = Struct.new(:loop, :sleep)
22
+
23
+ InfiniteLoop = Struct.new(:pause)
24
+ CountedLoop = Struct.new(:n, :pause)
25
+ TimerLoop = Struct.new(:hours, :minutes, :pause)
26
+ DeadlineLoop = Struct.new(:stop_time, :pause)
27
+
28
+ DeviceRef = Struct.new(:ref)
29
+ Light = Struct.new(:name)
30
+ Group = Struct.new(:name)
31
+ NoOp = Struct.new(:x)
24
32
  end
25
33
 
34
+ attr_accessor :name
35
+ attr_accessor :api_version
36
+ attr_accessor :data
37
+
26
38
  def valid?
27
39
  errors.empty?
28
40
  end
29
41
 
30
- alias_method :loop?, :loop
42
+ # Returns all light names hard-coded into the program
43
+ def light_names(node = data)
44
+ devices(AST::Light).uniq.map(&:name)
45
+ end
46
+
47
+ # Returns all group names hard-coded into the program
48
+ def group_names(node = data)
49
+ devices(AST::Group).uniq.map(&:name)
50
+ end
51
+
52
+ # Returns all device refs (e.g. $all, $1, $2) in the program
53
+ def device_refs(node = data)
54
+ devices(AST::DeviceRef).uniq.map(&:ref)
55
+ end
56
+
57
+ def errors(node = data)
58
+ node.children.reduce(node.errors) { |errors, child|
59
+ errors + child.errors
60
+ }
61
+ end
62
+
63
+ def warnings(node = data)
64
+ node.children.reduce(node.warnings) { |warnings, child|
65
+ warnings + child.warnings
66
+ }
67
+ end
68
+
69
+ private
70
+
71
+ def devices(type, node = data)
72
+ case node.instruction
73
+ when AST::Transition
74
+ node.instruction.devices.select { |d| d.is_a? type }
75
+ when AST::SerialControl, AST::ParallelControl
76
+ node.children.map { |n| devices type, n }.flatten
77
+ else
78
+ []
79
+ end
80
+ end
31
81
  end
32
82
  end
@@ -1,4 +1,4 @@
1
1
  module Huebot
2
2
  # Gem version
3
- VERSION = '0.4.0'
3
+ VERSION = '1.0.0'
4
4
  end
data/lib/huebot.rb CHANGED
@@ -1,31 +1,16 @@
1
- require 'hue'
2
-
3
1
  module Huebot
2
+ Error = Class.new(StandardError)
3
+
4
+ autoload :Config, 'huebot/config'
5
+ autoload :Client, 'huebot/client'
6
+ autoload :CLI, 'huebot/cli'
7
+ autoload :Bridge, 'huebot/bridge'
8
+ autoload :DeviceState, 'huebot/device_state'
9
+ autoload :Light, 'huebot/light'
10
+ autoload :Group, 'huebot/group'
4
11
  autoload :DeviceMapper, 'huebot/device_mapper'
5
12
  autoload :Program, 'huebot/program'
6
13
  autoload :Compiler, 'huebot/compiler'
7
14
  autoload :Bot, 'huebot/bot'
8
15
  autoload :VERSION, 'huebot/version'
9
-
10
- #
11
- # Struct for storing a program's Intermediate Representation and source filepath.
12
- #
13
- # @attr ir [Hash]
14
- # @attr filepath [String]
15
- #
16
- ProgramSrc = Struct.new(:ir, :filepath)
17
-
18
- #
19
- # Struct for specifying a Light input (id or name)
20
- #
21
- # @attr val [Integer|String] id or name
22
- #
23
- LightInput = Struct.new(:val)
24
-
25
- #
26
- # Struct for specifying a Group input (id or name)
27
- #
28
- # @attr val [Integer|String] id or name
29
- #
30
- GroupInput = Struct.new(:val)
31
16
  end
metadata CHANGED
@@ -1,15 +1,43 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: huebot
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.4.0
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jordan Hollinger
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-12-17 00:00:00.000000000 Z
12
- dependencies: []
11
+ date: 2023-12-18 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: minitest
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: '5.0'
20
+ type: :development
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - "~>"
25
+ - !ruby/object:Gem::Version
26
+ version: '5.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - "~>"
32
+ - !ruby/object:Gem::Version
33
+ version: '13.0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - "~>"
39
+ - !ruby/object:Gem::Version
40
+ version: '13.0'
13
41
  description: Declare and run YAML programs for Philips Hue devices
14
42
  email: jordan.hollinger@gmail.com
15
43
  executables:
@@ -19,29 +47,27 @@ extra_rdoc_files: []
19
47
  files:
20
48
  - README.md
21
49
  - bin/huebot
22
- - lib/hue.rb
23
- - lib/hue/LICENSE
24
- - lib/hue/bridge.rb
25
- - lib/hue/client.rb
26
- - lib/hue/editable_state.rb
27
- - lib/hue/errors.rb
28
- - lib/hue/group.rb
29
- - lib/hue/light.rb
30
- - lib/hue/scene.rb
31
- - lib/hue/translate_keys.rb
32
- - lib/hue/version.rb
33
50
  - lib/huebot.rb
34
51
  - lib/huebot/bot.rb
52
+ - lib/huebot/bridge.rb
35
53
  - lib/huebot/cli.rb
54
+ - lib/huebot/cli/helpers.rb
55
+ - lib/huebot/cli/runner.rb
56
+ - lib/huebot/client.rb
36
57
  - lib/huebot/compiler.rb
58
+ - lib/huebot/compiler/api_v1.rb
59
+ - lib/huebot/config.rb
37
60
  - lib/huebot/device_mapper.rb
61
+ - lib/huebot/device_state.rb
62
+ - lib/huebot/group.rb
63
+ - lib/huebot/light.rb
38
64
  - lib/huebot/program.rb
39
65
  - lib/huebot/version.rb
40
66
  homepage: https://github.com/jhollinger/huebot
41
67
  licenses:
42
68
  - MIT
43
69
  metadata: {}
44
- post_install_message:
70
+ post_install_message:
45
71
  rdoc_options: []
46
72
  require_paths:
47
73
  - lib
@@ -56,8 +82,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
56
82
  - !ruby/object:Gem::Version
57
83
  version: '0'
58
84
  requirements: []
59
- rubygems_version: 3.0.3.1
60
- signing_key:
85
+ rubygems_version: 3.4.1
86
+ signing_key:
61
87
  specification_version: 4
62
88
  summary: Orchestration for Hue devices
63
89
  test_files: []
data/lib/hue/LICENSE DELETED
@@ -1,22 +0,0 @@
1
- Copyright (c) 2013-2014 Sam Soffes, http://soff.es
2
-
3
- MIT License
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining
6
- a copy of this software and associated documentation files (the
7
- "Software"), to deal in the Software without restriction, including
8
- without limitation the rights to use, copy, modify, merge, publish,
9
- distribute, sublicense, and/or sell copies of the Software, and to
10
- permit persons to whom the Software is furnished to do so, subject to
11
- the following conditions:
12
-
13
- The above copyright notice and this permission notice shall be
14
- included in all copies or substantial portions of the Software.
15
-
16
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
- EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
- MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
- NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
- LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
- OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
- WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/lib/hue/bridge.rb DELETED
@@ -1,140 +0,0 @@
1
- module Hue
2
- class Bridge
3
- # ID of the bridge.
4
- attr_reader :id
5
-
6
- # Name of the bridge. This is also its uPnP name, so will reflect the
7
- # actual uPnP name after any conflicts have been resolved.
8
- attr_accessor :name
9
-
10
- # IP address of the bridge.
11
- attr_reader :ip
12
-
13
- # MAC address of the bridge.
14
- attr_reader :mac_address
15
-
16
- # IP Address of the proxy server being used.
17
- attr_reader :proxy_address
18
-
19
- # Port of the proxy being used by the bridge. If set to 0 then a proxy is
20
- # not being used.
21
- attr_reader :proxy_port
22
-
23
- # Software version of the bridge.
24
- attr_reader :software_version
25
-
26
- # Contains information related to software updates.
27
- attr_reader :software_update
28
-
29
- # An array of whitelisted user IDs.
30
- attr_reader :ip_whitelist
31
-
32
- # Network mask of the bridge.
33
- attr_reader :network_mask
34
-
35
- # Gateway IP address of the bridge.
36
- attr_reader :gateway
37
-
38
- # Whether the IP address of the bridge is obtained with DHCP.
39
- attr_reader :dhcp
40
-
41
- def initialize(client, hash)
42
- @client = client
43
- unpack(hash)
44
- end
45
-
46
- # Current time stored on the bridge.
47
- def utc
48
- json = get_configuration
49
- DateTime.parse(json['utc'])
50
- end
51
-
52
- # Indicates whether the link button has been pressed within the last 30
53
- # seconds.
54
- def link_button_pressed?
55
- json = get_configuration
56
- json['linkbutton']
57
- end
58
-
59
- # This indicates whether the bridge is registered to synchronize data with a
60
- # portal account.
61
- def has_portal_services?
62
- json = get_configuration
63
- json['portalservices']
64
- end
65
-
66
- def refresh
67
- json = get_configuration
68
- unpack(json)
69
- @lights = nil
70
- @groups = nil
71
- @scenes = nil
72
- end
73
-
74
- def lights
75
- @lights ||= begin
76
- json = JSON(Net::HTTP.get(URI.parse(base_url)))
77
- json['lights'].map do |key, value|
78
- Light.new(@client, self, key, value)
79
- end
80
- end
81
- end
82
-
83
- def add_lights
84
- uri = URI.parse("#{base_url}/lights")
85
- http = Net::HTTP.new(uri.host)
86
- response = http.request_post(uri.path, nil)
87
- (response.body).first
88
- end
89
-
90
- def groups
91
- @groups ||= begin
92
- json = JSON(Net::HTTP.get(URI.parse("#{base_url}/groups")))
93
- json.map do |id, data|
94
- Group.new(@client, self, id, data)
95
- end
96
- end
97
- end
98
-
99
- def scenes
100
- @scenes ||= begin
101
- json = JSON(Net::HTTP.get(URI.parse("#{base_url}/scenes")))
102
- json.map do |id, data|
103
- Scene.new(@client, self, id, data)
104
- end
105
- end
106
- end
107
-
108
- private
109
-
110
- KEYS_MAP = {
111
- :id => :id,
112
- :ip => :internalipaddress,
113
- :name => :name,
114
- :proxy_port => :proxyport,
115
- :software_update => :swupdate,
116
- :ip_whitelist => :whitelist,
117
- :software_version => :swversion,
118
- :proxy_address => :proxyaddress,
119
- :mac_address => :macaddress,
120
- :network_mask => :netmask,
121
- :portal_services => :portalservices,
122
- }
123
-
124
- def unpack(hash)
125
- KEYS_MAP.each do |local_key, remote_key|
126
- value = hash[remote_key.to_s]
127
- next unless value
128
- instance_variable_set("@#{local_key}", value)
129
- end
130
- end
131
-
132
- def get_configuration
133
- JSON(Net::HTTP.get(URI.parse("#{base_url}/config")))
134
- end
135
-
136
- def base_url
137
- "http://#{ip}/api/#{@client.username}"
138
- end
139
- end
140
- end