Skip to content

Instantly share code, notes, and snippets.

@henri
Last active April 2, 2023 09:22
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save henri/fd7e367d853a3970bdf6c077cf7765da to your computer and use it in GitHub Desktop.
Save henri/fd7e367d853a3970bdf6c077cf7765da to your computer and use it in GitHub Desktop.
Phillips Hue Hub Logging / Notification
#!/usr/bin/env ruby
# Copyright, All Rights Reserved Henri Shustak 2020
# Released under the GNU/GPL v3 or later licence
#
# requirements:
# - node.js - for hueadm - just install via package macnageer - eg brew.sh
# - hueadm - https://github.com/bahamas10/hueadm
# - datezone - http://www.fresse.org/dateutils/#datezone
#
#
# notes :
# This tools is built for the version of date which ships with macOS.
# If you are running on FreeBSD etc, you may need to change some of the date options for expected opperation.
# Operating system detection requires refactoring. This is basic implimentation for testing.
#
#
# - Version History
# - 2.2 - Introduced error capture and reporting for motion sensors which have no last update value
# - 2.1 - Introduced reduced tempature sensor polling option
# - 2.0 - Introduced error capture and reporting for switches which have no last update value
# - 1.9 - Minor performance improvemets
# - 1.8 - Improved battery reporting time managment and bug fixes
# - 1.7 - Added support for battery level reporting
# - 1.6 - Added support for GNU/LINUX date functions
# - 1.5 - Added support for tempature reporting
# - 1.4 - Added heart beat log reporting
# - 1.3 - Improved human readability with improved tab spacing between columns
# - 1.2 - Added support for sensors and switches with space in their name
# - 1.1 - Support added for gathering switch data
# - 1.0 - All configuration takes place in the script
#
#HUEHUB_IP = "192.168.1.xxx"
#HUEHUB_KEY = "long string of digits"
#LOCAL_TZ = "Pacific/Auckland"
# to get time zone from hub
# hueadm -U "#{HUEHUB_KEY}" -H "#{HUEHUB_IP}" config | grep timezone
# Time in seconds between polling the hue hub.
# Lower values will give higher resolution but have the draw back of
# using more resources on the hub and system running this script.
HUBPOLLTIME = "10"
# Heart beat interval.
# This will report a heart beat to the log file.
# Allows the log to be read and from the log determin if this deamon is working
HEARTBEAT = "60" #(60 is one miniute)
HEARTBEAT_LOOP_COUNT = HEARTBEAT.to_i / HUBPOLLTIME.to_i
@HEARTBEAT_COUNTDOWN = "#{HEARTBEAT_LOOP_COUNT}".to_i
# Tempature check interval
# Higher values will result in less tempature sensor polling, which
# will result in longer time between tempature sensor updates.
# A setting of zero "0" will disable this feature resulting in
# tempature updates which are probably close to the value which is
# specified for HUBPOLLTIME. If enabled you will need to lower
# your TIME_BEAT value to a much lower value so that the TEMP_POLLTIME
# actually happens at the time specified.
@TEMP_POLLTIME = "300" # (300 is 5 miniutes / 0 disables this feature)
# Battery check interval
BATTERY_POLLTIME = "86400" # (86400 is 24 hours)
# operating system we are running
OS = `uname`.chomp
# local time zone - global script time management
# time beat basically results in @TIME_NOW being less accurate but lowers system calls
# using a ruby time library may be a better approach, research and measurement required
# note that the @TIME_BEAT value should always be set to a value which is lower than
# the @TEMP_POLLTIME value.
@TIME_NOW = `date +%s`.chomp
@TIME_BEAT = "120" # (3600 is 1 hour / 180 is 3 miniutes)
@TIME_BEAT_LOOP_COUNT = @TIME_BEAT.to_i / HUBPOLLTIME.to_i
@TIME_BEAT_COUNTDOWN = "#{@TIME_BEAT_LOOP_COUNT}".to_i
@TIME_BEAT_START = "true"
@sensors = []
@switches = []
@tempature_sensors = []
# sanity checks and warnings
if @TIME_BEAT.to_i > @TEMP_POLLTIME.to_i && @TEMP_POLLTIME.to_i != 0 then
STDERR.puts "WARNING! : The configured \'@TIME_BEAT = \"#{@TIME_BEAT}\"\' value must be
decreased in order for \'@TEMP_POLLTIME = \"#{@TEMP_POLLTIME}\"\' value to be honored.
This is because the @TIME_NOW value would be updated at a slower rate than the TEMP_POLLTIME.
Alterativly, You could increase the @TEMP_POLLTIME value or set \'@TEMP_POLLTIME = \"0\"\' to disable
the tempature log limiting feature."
exit -20
end
# build list of motion sensors
sensor_list = `hueadm -U "#{HUEHUB_KEY}" -H "#{HUEHUB_IP}" sensors | grep ZLLPresence`
sensor_list.each_line do |s|
#current_sensor = s.split(" ")
current_sensor = s.split(/\s{2,}/)
@sensors.push({"id" => "#{current_sensor[0]}", "name" => "#{current_sensor[1]}", "type" => "#{current_sensor[2]}", "last_updated_gmt_time_raw" => "", "last_updated_local_time_raw" => "", "last_updated_local_time_seconds_epoch" => "", "last_updated_local_human" => "", "motion_present" => "", "sensor_updated" => "false", "battery_percentage" => "", "battery_next_update" => "", "battery_updated" => "false", "sensor_last_update_error" => "false" })
end
# build list of tempature sensors
tempature_sensor_list = `hueadm -U "#{HUEHUB_KEY}" -H "#{HUEHUB_IP}" sensors | grep ZLLTemperature`
tempature_sensor_list.each_line do |s|
current_tempature_sensor = s.split(/\s{2,}/)
@tempature_sensors.push({"id" => "#{current_tempature_sensor[0]}", "name" => "#{current_tempature_sensor[1]}", "type" => "#{current_tempature_sensor[2]}", "last_updated_gmt_time_raw" => "", "last_updated_local_time_raw" => "", "last_updated_local_time_seconds_epoch" => "", "last_updated_local_human" => "", "tempature" => "", "tempature_sensor_updated" => "false", "tempature_next_update" => "" })
end
# build list of switches (just the dimmer - battery operated 4 button ones for now)
switch_list = `hueadm -U "#{HUEHUB_KEY}" -H "#{HUEHUB_IP}" sensors | grep ZLLSwitch`
switch_list.each_line do |s|
current_switch = s.split(/\s{2,}/)
@switches.push({"id" => "#{current_switch[0]}", "name" => "#{current_switch[1]}", "type" => "#{current_switch[2]}", "last_updated_gmt_time_raw" => "", "last_updated_local_time_raw" => "", "last_updated_local_time_seconds_epoch" => "", "last_updated_local_human" => "", "button_event" => "", "switch_updated" => "false", "battery_percentage" => "", "battery_next_update" => "", "battery_updated" => "false", "switch_last_update_error" => "false" })
end
# Just an example if you need to get some data from a sensor.
# Below will list all the names of the sensors if that is something you want to do. Same logic can apply for lights
# sensors.each do |s|
# puts s["name"]
# end
def get_last_update_motion_from_sensors
@sensors.each do |s|
sensor_last_updated = s["last_updated_gmt_time_raw"]
sensor_battery_next_update = s['battery_next_update']
# get sensor data
last_update_gmt =`hueadm -U "#{HUEHUB_KEY}" -H "#{HUEHUB_IP}" sensor #{s["id"]} | grep lastupdated: | awk -F ": " '{print $2}' | tr -d "'"`.chomp
#last_update_local =`datezone "#{LOCAL_TZ}" --from-zone=UTC "#{last_update_gmt}" | awk -F "+" '{print $1}'`.chomp
# catch errors on sensors - # important for disabled sensors during startup
if "#{last_update_gmt}" != "none" then
last_update_local =`datezone "#{LOCAL_TZ}" --from-zone=UTC "#{last_update_gmt}" | awk -F "+" '{print $1}'`.chomp
last_update_local_error = "false"
else
# Sensor ERROR! state detected
last_update_local = "2022-08-27T22:39:30" # made up date so other conversions do not fail - requires additional testing
last_update_local_error = "true"
end
if "#{OS.to_s}" == "Darwin" then
last_updated_local_time_seconds_epoch = `date -j -f "%Y-%M-%dT%H:%M:%S" #{last_update_local} +%s`.chomp
last_update_local_human_readable = `date -j -f "%Y-%M-%dT%H:%M:%S" #{last_update_local}`.chomp
else
last_updated_local_time_seconds_epoch = `date "+%s" -d #{last_update_local}`.chomp
last_update_local_human_readable = `date "+%a %d %b %Y %T %Z" -d #{last_update_local}`.chomp
end
# check if this is the first time the sensor has been updated in memory
if "#{sensor_last_updated}" == "" || "#{sensor_last_updated}" != "#{last_update_gmt}" || "#{last_update_local_error}" == "true" then
# update the sensor data sotred in the memory hash
s['last_updated_gmt_time_raw'] = "#{last_update_gmt}"
s['last_updated_local_time_raw'] = "#{last_update_local}"
s['last_updated_local_time_seconds_epoch'] = "#{last_updated_local_time_seconds_epoch}"
s['last_updated_local_human'] = "#{last_update_local_human_readable}"
# if the sensor has been updated also check the sensor for presence
sensor_current_presence = `hueadm -U "#{HUEHUB_KEY}" -H "#{HUEHUB_IP}" sensor #{s["id"]} | grep presence | awk -F ": " '{print $2}'`.chomp
s['motion_present'] = "#{sensor_current_presence}"
# update sensor update notification status
if "#{last_update_local_error}" != "true" then
# update sensor update notification status
s['sensor_updated'] = "true"
s['sensor_last_update_error'] = "false"
else
s['sensor_last_update_error'] = "true"
end
end
# check battery percentage
if "#{sensor_battery_next_update}" == "" || @TIME_NOW.to_i > sensor_battery_next_update.to_i then
s['battery_percentage'] = `hueadm -U "#{HUEHUB_KEY}" -H "#{HUEHUB_IP}" sensor #{s["id"]} | grep "battery: " | awk '{print $2}'`.chomp
s['battery_next_update'] = @TIME_NOW.to_i + BATTERY_POLLTIME.to_i
s['battery_updated'] = "true"
end
end
end
def get_last_update_from_tempature_sensors
@tempature_sensors.each do |s|
# tempature_sensor_next_update will only has a value if TEMP_POLL time is set
tempature_sensor_next_update = s["tempature_next_update"]
# if there is a TEMP_POLL time set wait for it otherwise proceed without TEMP_POLL delay
if "#{tempature_sensor_next_update}" == "" || @TIME_NOW.to_i > tempature_sensor_next_update.to_i then
if @TEMP_POLLTIME.to_i != 0 then
s['tempature_next_update'] = @TIME_NOW.to_i + @TEMP_POLLTIME.to_i
end
tempature_sensor_last_updated = s["last_updated_gmt_time_raw"]
# get sensor data
last_update_gmt =`hueadm -U "#{HUEHUB_KEY}" -H "#{HUEHUB_IP}" sensor #{s["id"]} | grep lastupdated: | awk -F ": " '{print $2}' | tr -d "'"`.chomp
last_update_local =`datezone "#{LOCAL_TZ}" --from-zone=UTC "#{last_update_gmt}" | awk -F "+" '{print $1}'`.chomp
if "#{OS.to_s}" == "Darwin" then
last_updated_local_time_seconds_epoch = `date -j -f "%Y-%M-%dT%H:%M:%S" #{last_update_local} +%s`.chomp
last_update_local_human_readable = `date -j -f "%Y-%M-%dT%H:%M:%S" #{last_update_local}`.chomp
else
last_updated_local_time_seconds_epoch = `date "+%s" -d #{last_update_local}`.chomp
last_update_local_human_readable = `date "+%a %d %b %Y %T %Z" -d #{last_update_local}`.chomp
end
# check if this is the first time the sensor has been updated in memory
# has the sensor has been updated also check the sensor for tempature
if "#{tempature_sensor_last_updated}" == "" || "#{tempature_sensor_last_updated}" != "#{last_update_gmt}" then
# update the sensor data stored in the memory hash
s['last_updated_gmt_time_raw'] = "#{last_update_gmt}"
s['last_updated_local_time_raw'] = "#{last_update_local}"
s['last_updated_local_time_seconds_epoch'] = "#{last_updated_local_time_seconds_epoch}"
s['last_updated_local_human'] = "#{last_update_local_human_readable}"
# check the sensor for updated tempature data
tempature_sensor_current_tempature = `hueadm -U "#{HUEHUB_KEY}" -H "#{HUEHUB_IP}" sensor #{s["id"]} | grep temperature | awk -F ": " '{print $2}' | head -n 1`.chomp
s['tempature'] = "#{tempature_sensor_current_tempature}".chomp
# update sensor updated status and next poll time (if enabled)
s['tempature_sensor_updated'] = "true"
end
end
end
end
def get_last_button_event_from_switches
@switches.each do |s|
switch_last_updated = s["last_updated_gmt_time_raw"]
switch_battery_next_update = s['battery_next_update']
# get switch data
last_update_gmt =`hueadm -U "#{HUEHUB_KEY}" -H "#{HUEHUB_IP}" sensor #{s["id"]} | grep lastupdated: | awk -F ": " '{print $2}' | tr -d "'"`.chomp
if "#{last_update_gmt}" != "none" then
last_update_local =`datezone "#{LOCAL_TZ}" --from-zone=UTC "#{last_update_gmt}" | awk -F "+" '{print $1}'`.chomp
last_update_local_error = "false"
else
# Switch ERROR! state detected
last_update_local = "2022-08-27T22:39:30" # made up date so other conversions do not fail - requires additional testing
last_update_local_error = "true"
end
if "#{OS.to_s}" == "Darwin" then
last_updated_local_time_seconds_epoch = `date -j -f "%Y-%M-%dT%H:%M:%S" #{last_update_local} +%s`.chomp
last_update_local_human_readable = `date -j -f "%Y-%M-%dT%H:%M:%S" #{last_update_local}`.chomp
else
last_updated_local_time_seconds_epoch = `date "+%s" -d #{last_update_local}`.chomp
last_update_local_human_readable = `date "+%a %d %b %Y %T %Z" -d #{last_update_local}`.chomp
end
# check if this is the first time the switch has been updated in memory
if "#{switch_last_updated}" == "" || "#{switch_last_updated}" != "#{last_update_gmt}" then
# update the switch data sotred in the memory hash
s['last_updated_gmt_time_raw'] = "#{last_update_gmt}"
s['last_updated_local_time_raw'] = "#{last_update_local}"
s['last_updated_local_time_seconds_epoch'] = "#{last_updated_local_time_seconds_epoch}"
s['last_updated_local_human'] = "#{last_update_local_human_readable}"
# if the button has been updated also check the button event
switch_button_event = `hueadm -U "#{HUEHUB_KEY}" -H "#{HUEHUB_IP}" sensor #{s["id"]} | grep " buttonevent: " | awk -F ": " '{print $2}'`.chomp
s['button_event'] = "#{switch_button_event}"
if "#{switch_last_updated}" != "" || "#{last_update_local_error}" == "true" then
if "#{last_update_local_error}" != "true" then
# update sensor update notification status
s['switch_updated'] = "true"
s['switch_last_update_error'] = "false"
else
s['switch_last_update_error'] = "true"
end
end
# check battery percentage
if "#{switch_battery_next_update}" == "" || @TIME_NOW.to_i > switch_battery_next_update.to_i then
s['battery_percentage'] = `hueadm -U "#{HUEHUB_KEY}" -H "#{HUEHUB_IP}" sensor #{s["id"]} | grep "battery: " | awk '{print $2}'`.chomp
s['battery_next_update'] = @TIME_NOW.to_i + BATTERY_POLLTIME.to_i
s['battery_updated'] = "true"
end
end
end
end
# Okay we will now poll the hue hub periodically and update the output based on the sensors information.
# puts "type\t\t name \t\t\t event \t updated"
while true
`sleep #{HUBPOLLTIME}`
# time beat - used to update @TIME_NOW
@TIME_BEAT_COUNTDOWN -= 1
if @TIME_BEAT_COUNTDOWN <= 0 || @TIME_BEAT_START == "true" then
@TIME_BEAT_START = "false"
@TIME_BEAT_COUNTDOWN = @TIME_BEAT_LOOP_COUNT
@TIME_NOW = `date +%s`.chomp
# puts "timebeat \t\t\t \t\t \t\t #{@TIME_NOW}"
end
# report heart beat to log - used to heart beat to the log
@HEARTBEAT_COUNTDOWN -= 1
if @HEARTBEAT_COUNTDOWN <= 0 then
heart_beat_time=`date`
@HEARTBEAT_COUNTDOWN = HEARTBEAT_LOOP_COUNT
puts "heartbeat \t\t\t \t\t \t\t #{heart_beat_time}"
end
get_last_update_motion_from_sensors
@sensors.each do |s|
sensor_updated = s["sensor_updated"]
sensor_name = s["name"]
sensor_motion = s["motion_present"]
sensor_last_update = s["last_updated_local_human"]
sensor_battery_updated = s["battery_updated"]
sensor_last_update_error_detected = s["sensor_last_update_error"]
if "#{sensor_updated}" == "true" || "#{sensor_battery_updated}" == "true" then
# make the tabs pretty - this is ugly and should be sorted with a better appraoch
if sensor_name.length <= 5 then
sensor_name_tab="\t\t\t\t"
else
if sensor_name.length <= 14 then
sensor_name_tab="\t\t\t"
else
sensor_name_tab="\t\t"
end
end
end
if "#{sensor_updated}" == "true" then
#update sensor so it is no loner true because we are reporting the change
s["sensor_updated"] = "false"
puts "motion\t\t #{sensor_name} #{sensor_name_tab} #{sensor_motion} \t\t #{sensor_last_update}"
end
# list battery information and any errors
if "#{sensor_battery_updated}" == "true" then
s["battery_updated"] = "false"
sensor_battery_percentage = s["battery_percentage"]
puts "m-battery\t #{sensor_name} #{sensor_name_tab} #{sensor_battery_percentage} \t\t #{sensor_last_update}"
# report errors
if "#{sensor_last_update_error_detected}" == "true" then
sensor_last_update_error_current_time = `date`
puts "m-error\t\t #{sensor_name} #{sensor_name_tab} \t\t #{sensor_last_update_error_current_time}"
end
end
end
get_last_update_from_tempature_sensors
@tempature_sensors.each do |s|
tempature_sensor_updated = s["tempature_sensor_updated"]
tempature_sensor_name = s["name"].chomp(" Temp")
tempature_sensor_tempature = s["tempature"]
tempature_sensor_last_update = s["last_updated_local_human"]
if "#{tempature_sensor_updated}" == "true" then
#update sensor so it is no loner true because we are reporting the change
s["tempature_sensor_updated"] = "false"
# make the tabs pretty - this is ugly and shoudl be sorted with a better appraoch
if tempature_sensor_name.length <= 5 then
tempature_sensor_name_tab="\t\t\t\t"
else
if tempature_sensor_name.length <= 14 then
tempature_sensor_name_tab="\t\t\t"
else
if tempature_sensor_name.length <= 19 then
tempature_sensor_name_tab="\t\t"
else
tempature_sensor_name_tab="\t"
end
end
end
puts "tempature\t #{tempature_sensor_name} #{tempature_sensor_name_tab} #{tempature_sensor_tempature} \t\t #{tempature_sensor_last_update}"
end
end
get_last_button_event_from_switches
@switches.each do |s|
switch_updated = s["switch_updated"]
switch_name = s["name"]
switch_button_event = s["button_event"]
switch_last_update = s["last_updated_local_human"]
switch_battery_updated = s["battery_updated"]
switch_last_update_error_detected = s["switch_last_update_error"]
if "#{switch_updated}" == "true" || "#{switch_battery_updated}" == "true" then
# make the tabs pretty - this is ugly and shoudl be sorted with a better appraoch
if switch_name.length <= 5 then
switch_name_tab="\t\t\t\t"
else
if switch_name.length <= 14 then
switch_name_tab="\t\t\t"
else
switch_name_tab="\t\t"
end
end
end
if "#{switch_updated}" == "true" then
# update sensor so it is no loner true because we are reporting the change
s["switch_updated"] = "false"
puts "switch\t\t #{switch_name} #{switch_name_tab} #{switch_button_event} \t\t #{switch_last_update}"
end
# list battery information and any errors
if "#{switch_battery_updated}" == "true" then
s["battery_updated"] = "false"
switch_battery_percentage = s["battery_percentage"]
puts "s-battery\t #{switch_name} #{switch_name_tab} #{switch_battery_percentage} \t\t #{switch_last_update}"
# report errors
if "#{switch_last_update_error_detected}" == "true" then
switch_last_update_error_current_time = `date`
puts "s-error\t\t #{switch_name} #{switch_name_tab} \t\t #{switch_last_update_error_current_time}"
end
end
end
end
exit
This is the start of what may become a more comprehensive project for monitoring and logging Philips
Hue Motion Sensor Data and other devices connected to the Hue Hub.
At this stage the project is a simple script which may be invoked to start logging various data which is availbel from the Phillips Hue Hub (including motion sensor data).
By parsing the generated data it is possible to trigger events / notifications or activate other systems via the network / connected software / outputs.
This implimentation is a starting point and as time permits / people have interst, I am open to building a
project which is able to be more comprehensive with built in notifications / deamons / installation script.
At this stage, if you are looking to get notifiationcs on a phone, the best bet is to take a look at something like
PushOver, you just need to scan the output of this monitoring script and then send push a notification if you feel like
paramerers are being meet which would justify a notification such as motion being present on a certain sensor
at a certain time.
If you want to write a log file, just start the script and use the tee command to send the data out to a log file.
It is of course very likely you will want to store this data in a database and that can be arranged with a custom output
parser which will store any realivent data in what ever way makes sense to you.
Before running the script, you will need to set the API key and HueHub IP / DNS name within the script as well as
your local regions local. This early version has no config file, you just edit the script and then run it.
Details on different local names is availble from the datezone website which is used to perform the time zone
conversions from UTC to your local time. Please note that variuous dates and times are stored in the internal memory
data structrue. As such, if you want to extract something else for the log it is a matter of just changing the output
data.
Ideas and feed back are welcomed. If you have a hue hub and motion sensors and would like to see this develop into a more
mature project, then let me know. Also, just ping me if you are using this it would be good to know if I should develop this
for personal use or if there are others who would like to benifit from the development. At present the script is not sending
any telematry.
If you are using tempature data to control enviroment tempature, it is advised that you alter or even disable to TEMP_POLL
value so that you have more frequent or less frequent tempature updates, depending on your update frequency requirments.
# Ideas for Future Versions
(1) Have a configrufor the hue hub settings and tation file or just rely on hueadm he time zone could be gathered
when the script is started from the system. Just ome ideas to improve future version. (6)
(2) Possibly add light sensor data from motion sensors to the output.
(3) Refactoring varios aspects.
(4) TimeZone reading from system.
(5) Improve output grouping of battery status information maybe with a stack which is pushed and popped or some other approach
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment