huebot 0.4.0 → 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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