This repository has been archived on 2020-04-11. You can view files and clone it, but cannot push or open issues or pull requests.
hued/hued

320 lines
8.9 KiB
Plaintext
Raw Normal View History

2014-01-11 22:33:15 +01:00
#!/usr/bin/env ruby
2014-11-15 21:13:09 +01:00
# encoding: UTF-8
#
# hued - Philips (friends of) hue policy daemon
#
# Hued is Copyright © 2011 Paul van Tilburg <paul@luon.net>
#
# This program is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License as published by the
# Free Software Foundation; either version 2 of the License, or (at your
# option) any later version.
2014-01-11 22:33:15 +01:00
require "chronic"
require "eventmachine"
2014-01-11 22:33:15 +01:00
require "huey"
require "logger"
2014-11-15 21:12:51 +01:00
require "optparse"
require "pp"
2014-01-11 22:33:15 +01:00
# The main engine class
class Hued
2014-01-11 22:33:15 +01:00
2014-11-15 21:13:09 +01:00
# Scenes
Scenes = {}
2014-01-11 22:33:15 +01:00
2014-11-15 21:12:51 +01:00
# Program version
VERSION = "0.0.1"
# The rule class
class Rule
attr_reader :name, :conditions, :tigger, :priority, :events, :scene
def initialize(name, log, entry)
@name = name
@log = log
@validity = false
@conditions = entry["conditions"] || []
@trigger = entry["trigger"].nil? ? true : entry["trigger"]
@triggered = false
@priority = entry["priority"] || 0
@events = []
if entry["events"]
entry["events"].each do |ev_name|
event = Huey::Event.find(ev_name)
if event.nil?
@log.warn "Could not find event #{ev_name} for rule \"#{name}\", ignoring!"
else
@events << event
end
end
elsif entry["scene"]
@scene = entry["scene"] if Scenes.has_key? entry["scene"]
if @scene.nil?
@log.warn "Could not find scene #{entry["scene"]} for rule \"#{name}\", ignoring!"
end
else
raise ArgumentError, "You must supply either an even or scene name"
end
end
def valid?
# Determine validity
prev_validity = @validity
@validity = test_conditions
# Reset the triggered flag if the this is a trigger rule, but it is
# no longer valid
@triggered = false if @trigger and prev_validity and !@validity
@validity
end
def triggered?
@triggered
end
# If this is a trigger rule, it should only be triggerd if it wasn't
# valid in a previous validity check
def trigger?
if @trigger
!@triggered
else
@validity
end
end
def execute
@triggered = true
events = if @scene
@log.info "Executing scene: #{@scene}"
events = Scenes[@scene]
elsif @events
@events
else
@log.info "No scene or events found, skipping execution"
[]
end
events.each_with_index do |event, idx|
if event.name
@log.info "Executing event: #{event.name}!"
else
@log.info "Executing event #{idx}"
end
retry_count = 0
begin
event.execute
rescue Huey::Errors::BulbOff
if retry_count > 4
@log.warn "One of the lights is still off, ignoring event"
else
@log.warn "One of the lights was off, retrying..."
event.group.bulbs.each { |bulb| bulb.on = false }
retry_count += 1
retry
end
rescue Huey::Errors::Error => e
@log.error "Error while executing event (#{e.class}): #{e.message}"
end
end
end
2014-11-15 21:13:09 +01:00
#######
private
def test_conditions
# If there are no conditions, the rule is always valid
return true if @conditions.empty?
@conditions.map do |cond|
if cond.is_a? Hash
cond_name, cond_value = cond.to_a.first
case cond_name
when "from"
Time.now >= Chronic.parse(cond_value)
when "until"
Time.now <= Chronic.parse(cond_value)
when "found host"
system("ping -W3 -c1 -q #{cond_value} > /dev/null")
end
else
@log.warn "Unknown condition type/form #{cond.inspect}"
end
end.all?
end
end # class Hued::Rule
def initialize(options = {})
@options = options
@log = Logger.new($stdout)
@log.progname = "hued"
@log.level = options[:debug] ? Logger::DEBUG : Logger::INFO
@log.formatter = proc do |severity, datetime, progname, msg|
"#{datetime.strftime('%b %d %X')} #{progname}[#{$$}]: #{msg}\n"
end
configure
discover
load
@log.info "Started successfully!"
end
def configure
@log.info "Starting..."
bridge_cfg = File.open("bridge.yml") { |file| YAML.load(file) }
Huey.configure do |cfg|
cfg.hue_ip = bridge_cfg["ip"]
cfg.uuid = bridge_cfg["user"]
2014-11-15 21:12:51 +01:00
if @options[:hue_debug]
cfg.logger = @log
else
# Use the default logger and make it shut up
cfg.logger.level = Logger::FATAL
end
end
@log.info "Configured bridge connection"
end
def discover
@log.info "Discovering lights..."
@lights = Huey::Bulb.all
@lights.each do |light|
@log.info "* Found light #{light.id}: #{light.name}"
2014-11-15 21:12:51 +01:00
light.alert! if @options[:blink]
end
@log.info "Found #{@lights.count} light#{"s" unless @lights.count == 1}"
@log.info "Discovering groups..."
@groups = Huey::Group.all
@groups.each do |group|
@log.info "* Found group #{group.id}: #{group.name} with " \
"lights #{group.bulbs.map(&:id).join(", ")}"
end
@log.info "Found #{@groups.count} group#{@groups.count != 1 || "s"}"
end
def refresh!
@log.debug "Refreshing lights..."
@lights.each { |light| light.reload }
@log.debug "Refreshed #{@lights.count} light#{"s" unless @lights.count == 1}"
end
def load
@log.info "Loading events..."
@events = Huey::Event.import("events.yml")
@events.each do |event|
event.actions["on"] = true if event.actions["on"].nil?
@log.info "* Loaded event: #{event.name}"
end
@log.info "Loaded #{@events.count} event#{"s" unless @events.count == 1}"
@log.info "Loading scenes..."
@scenes = {}
YAML.load_file("scenes.yml").each do |name, entry|
@scenes[name] = entry.map do |ev_options|
# Keys should be symbols
options = ev_options.inject({}) { |opts, (k, v)| opts[k.to_sym] = v; opts }
event = Huey::Event.new(options)
event.actions["on"] = true if event.actions["on"].nil?
event
end
Scenes[name] = @scenes[name]
@log.info "* Loaded scene: #{name}"
end
@log.info "Loaded #{@scenes.count} scene#{"s" unless @scenes.count == 1}"
@log.info "Loading rules"
@rules = YAML.load_file("rules.yml").map do |name, entry|
Rule.new(name, @log, entry)
end
@rules.each do |rule|
@log.info "* Loaded rule: #{rule.name}"
end
@log.info "Loaded #{@rules.count} rule#{"s" unless @rules.count == 1}"
end
def execute
@log.debug "Looking for valid rules..."
valid_rules = @rules.select(&:valid?)
if valid_rules.empty?
@log.debug "None found"
return
else
@log.debug "There #{valid_rules.count == "1" ? "is" : "are"} " \
"#{valid_rules.count} valid " \
"rule#{"s" unless valid_rules.count == 1}"
end
prio_map = valid_rules.group_by(&:priority)
prios = prio_map.keys.sort
prios.each do |prio|
prio_rules = prio_map[prio]
@log.debug "* Rule#{"s" unless prio_rules.count == 1} with prioity #{prio}: " +
prio_rules.map(&:name).join(", ")
end
valid_rules_with_highest_prio = prio_map[prios.last]
if valid_rules != valid_rules_with_highest_prio
@log.debug "There #{valid_rules_with_highest_prio.count == 1 ? "is" : "are"} " \
"only #{valid_rules_with_highest_prio.count} valid " \
"rule#{"s" unless valid_rules_with_highest_prio.count == 1} " \
"for the hightest priority #{prios.last}"
end
valid_rules_with_highest_prio.each do |rule|
if rule.trigger?
@log.info "Rule \"#{rule.name}\" is valid and should be triggered, executing..."
rule.execute
else
@log.debug "Rule \"#{rule.name}\" is valid, but should not be triggered"
end
end
end
end # class Hued
# Option parsing
2014-11-15 21:12:51 +01:00
options = {blink: true}
opt_parser = OptionParser.new do |opts|
opts.banner = "Usage: hued [options]"
opts.separator ""
opts.on("--[no-]blink", "blink lights when discovered on startup") do |bl|
options[:blink] = bl
end
opts.on("-d", "--debug", "log debug output") do
options[:debug] = true
end
opts.on("--hue-debug", "log hue bridge communication output") do
options[:hue_debug] = true
end
opts.on_tail("-h", "--help", "show this help message") do
puts opts
exit
end
opts.on_tail("-v", "--version", "show version") do
puts "Hued version #{Hued::VERSION}"
exit
end
end
begin
opt_parser.parse!
rescue OptionParser::InvalidOption => e
warn e.message
abort opt_parser.to_s
end
2014-01-11 22:33:15 +01:00
# Create the main engine and trigger it periodically
hued = Hued.new(options)
EM.run do
EM.add_periodic_timer(10) { hued.execute }
EM.add_periodic_timer(300) { hued.refresh! }
2014-01-11 22:33:15 +01:00
end