Navigation Menu

Skip to content

Instantly share code, notes, and snippets.

@ccutrer
Last active December 9, 2023 01:57
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 3 You must be signed in to fork a gist
  • Save ccutrer/8894f62eadeaae145eebbab0a49adb1f to your computer and use it in GitHub Desktop.
Save ccutrer/8894f62eadeaae145eebbab0a49adb1f to your computer and use it in GitHub Desktop.
EverLights API
The app always sends a nocache param, which appears to be time since the app launched. It's ignored by the control box.
Color is just RGB. 255,255,255 = 0xffffff. 0,255,0 = 0x00ff00
Modes:
all modes can take a value between 0-255. only one of tw, cf, cr, wi, ra can be set at once
tw: twinkle
fa: fade
bl: blink
st: strobe
cf: chase forward
cr: chase reverse
wi: wipe
ra: random
rb: ???
For pattern names, spaces are saved as underscores (you can't save underscores)
User-Agent: everlights2/7 CFNetwork/978.0.7 Darwin/18.6.0
GET /status/get?nocache=534.999015
{"stsCnt":20,"ch1Cnt":402,"ch2Cnt":201,"version":"2.3.0","mac":"2C:3A:E8:FF:FF:FF","autopilot":0,"epoch":1560017691,"tzOffset":-7,"stsActive":1,"ch1Active":0,"ch2Active":0,"stsInd":1,"lightingStatus":0,"ssid":"Cutrer","rssi":-58,"ip":"192.168.10.10","mode":1,"status":"connected","rebootCnt":21,"loopsPerSecond":13422,"writeDuration":19327}
GET /epoch/set?epoch=1560017696&nocache=567.472399
{"success":true}
GET /ptrn/all
{"patterns":["NEW_YEAR'S","VALENTINE'S_DAY","ST_PATRICK'S_DAY","EASTER","MOTHER'S_DAY","FATHER'S_DAY","HALLOWEEN","VETERANS_DAY","THANKSGIVING","CHRISTMAS","Color_splat","My_Christmas","INDEPENDENCE_DAY"],"cnt":13}
GET /ptrn/get?id=NEW_YEAR'S
{"len":3,"ptrn":[16733952,16777215,16733952],"modes":{"fa":0,"bl":0,"st":0,"cf":0,"cr":0,"wi":0,"ra":0,"tw":0,"rb":0},"name":"NEW_YEAR'S"}
GET /sch/getchannel?channel=1
{"schedules":[]}
GET /ptrn/set?channel=3&data=%7B%22len%22:4,%22ptrn%22:[16777215,16711680,16777215,45056],%22modes%22:%7B%22fa%22:0,%22bl%22:0,%22st%22:0,%22cf%22:0,%22cr%22:0,%22wi%22:0,%22ra%22:0,%22tw%22:0,%22rb%22:0%7D,%22name%22:%22%22%7D
{"success":true}
GET /status/get
{"stsCnt":20,"ch1Cnt":402,"ch2Cnt":201,"version":"2.3.0","mac":"2C:3A:E8:FF:FF:FF","autopilot":0,"epoch":1560018587,"tzOffset":-7,"stsActive":1,"ch1Active":1,"ch2Active":1,"stsInd":1,"lightingStatus":0,"ssid":"Cutrer","rssi":-58,"ip":"192.168.10.10","mode":1,"status":"connected","rebootCnt":21,"loopsPerSecond":13099,"writeDuration":19660}
turn lights off
GET /ptrn/clear?channel=0
GET /ptrn/clear?channel=1
GET /ptrn/clear?channel=2
GET /ptrn/clear?channel=3
save pattern
GET /ptrn/sv?id=My_Christmas2&data=%7B%22len%22:4,%22ptrn%22:[16777215,16711680,16777215,45056],%22modes%22:%7B%22fa%22:0,%22bl%22:0,%22st%22:0,%22cf%22:0,%22cr%22:0,%22wi%22:0,%22ra%22:154,%22tw%22:0,%22rb%22:0%7D,%22name%22:%22My_Christmas2%22%7D
delete a pattern
GET /ptrn/del?id=MY_CHRUSTMAS
rule "Any rule that runs daily at sunset or whenever you want the lights to come on"
when
...
then
// Calculate if Everlights should be on
var holiday = executeCommandLine("/var/lib/openhab2/holidays.rb", 500)
if (holiday != "") {
Everlights_Preset.sendCommand(holiday)
Everlights_Switch.sendCommand(ON)
}
end
// don't forget a rule to turn them off
Switch Everlights_Switch "Everlights" <light> (gAllOff)
String EverlightsZone1_Data
String EverlightsZone2_Data
Group gEverlights
Group gEverlightsExclusive
String Everlights_Preset "Pattern"
Switch Everlights_PatternChanging
Number Everlights_Count "Pattern Length [%s]" (gEverlights)
Color Everlights1_RGB "Color 1" (gEverlights)
Color Everlights2_RGB "Color 2" (gEverlights)
Color Everlights3_RGB "Color 3" (gEverlights)
Color Everlights4_RGB "Color 4" (gEverlights)
Color Everlights5_RGB "Color 5" (gEverlights)
Color Everlights6_RGB "Color 6" (gEverlights)
Color Everlights7_RGB "Color 7" (gEverlights)
Color Everlights8_RGB "Color 8" (gEverlights)
Color Everlights9_RGB "Color 9" (gEverlights)
Color Everlights10_RGB "Color 10" (gEverlights)
Number Everlights_Twinkle "Twinkle" (gEverlights, gEverlightsExclusive)
Number Everlights_Fade "Fade" (gEverlights)
Number Everlights_Blink "Blink" (gEverlights)
Number Everlights_Strobe "Strobe" (gEverlights)
Number Everlights_Chase "Chase" (gEverlights, gEverlightsExclusive)
Number Everlights_ChaseReverse "Reverse Chase" (gEverlights, gEverlightsExclusive)
Number Everlights_Wipe "Wipe" (gEverlights, gEverlightsExclusive)
Number Everlights_Random "Random" (gEverlights, gEverlightsExclusive)
# frozen_string_literal: true
require "cgi"
require "json"
require "net/http"
gemfile do
source "https://rubygems.org/"
gem "activesupport", "~> 7.0", require: false
end
require "active_support/core_ext/enumerable"
require "active_support/core_ext/object/blank"
require "active_support/core_ext/object/try"
EVERLIGHTS_HOST = "everlights.cutrer.network"
def normalize_pattern(pattern, reverse: false)
modes = { fa: 0, bl: 0, st: 0, cf: 0, cr: 0, wi: 0, ra: 0, tw: 0, rb: 0 }.merge(pattern[:modes] || {})
pattern = pattern[:ptrn]
if reverse
temp = modes[:cf]
modes[:cf] = modes[:cr]
modes[:cr] = temp
pattern = pattern.reverse
end
CGI.escape({ len: pattern.length, ptrn: pattern, modes: }.to_json)
end
MODES = {
tw: "Twinkle",
fa: "Fade",
blink: "Blink",
st: "Strobe",
cf: "Chase",
cr: "ChaseReverse",
wi: "Wipe",
ra: "Random"
}.freeze
def calculate_data(reverse: false)
pattern = {}
modes = pattern[:modes] = {}
pattern[:ptrn] = Array.new(Everlights_Count.state.to_i) do |i|
item = items["Everlights#{i + 1}_RGB"]
next 0xffffff unless item.state?
item.state.rgb
end
MODES.each do |(key, name)|
modes[key] = items["Everlights_#{name}"].state.to_i * 255 / 100
end
normalize_pattern(pattern, reverse:)
end
def update_everlights(zone)
return if items["EverlightsZone#{zone}_Data"].state.to_s.empty?
Net::HTTP.get(EVERLIGHTS_HOST,
"/ptrn/set?channel=#{zone}&data=#{items["EverlightsZone#{zone}_Data"].state}")
end
received_command Everlights_Switch, command: ON do
2.times { |zone| update_everlights(zone + 1) }
end
received_command Everlights_Switch, command: OFF do
Net::HTTP.get(EVERLIGHTS_HOST, "/ptrn/clear?channel=3")
end
2.times do |zone|
zone += 1
changed items["EverlightsZone#{zone}_Data"] do |_event|
next unless Everlights_Switch.on?
update_everlights(zone)
end
end
changed gEverlights.members do
next unless Everlights_Count.state?
unless Everlights_PatternChanging.on?
EverlightsZone1_Data.update(calculate_data)
EverlightsZone2_Data.update(calculate_data(reverse: true))
Everlights_Preset.update(NULL)
end
end
changed gEverlightsExclusive.members do |event|
gEverlightsExclusive.members.each do |item|
item.update(0) unless item == event.item
end
end
PATTERNS = {
"Valentine's Day" => {
colors: %w[#ff0000 #ffffff #ff0000 #fc00ff],
chase: 40
},
"St. Patrick's Day" => {
colors: %w[#00ff41 #00ff41 #00ff00],
chase: 40
},
"Easter" => {
colors: %w[#fd13ff #f9ff20 #ff0141 #5aff12 #2949ff]
},
"Patriotic" => {
colors: %w[#0000ff #ff0000 #ffffff #fc0000 #0000ff #000000],
chase: 40
},
"Halloween" => {
colors: %w[#ff2900 #ff2900 #7300ff #004f00]
},
"Thanksgiving" => {
colors: %w[#ff8721 #ff4b00 #990000 #ff4b00]
},
"Christmas" => {
colors: %w[#ffd6aa #ff0000 #ffd6aa #00b000]
},
"Olympics" => {
colors: %w[#0020c8 #ffa000 #303030 #005000 #800000 #000000 #000000 #000000]
},
"Rainbow" => {
colors: %w[#ff0000 #ff2900 #ffc000 #00ff00 #0000ff #3200ff]
}
}.freeze
STYLES = %i[fade blink strobe].freeze
EXCLUSIVE_STYLES = %i[twinkle chase chase_reverse wipe random].freeze
rule "Everlights Preset" do
changed Everlights_Preset
received_command Everlights_Preset
run do |event|
pattern_name = event.try(:state) || event.try(:command)
pattern = PATTERNS[pattern_name.to_s]
next unless pattern
Everlights_PatternChanging.update(ON)
Everlights_Count.update(pattern[:colors].length)
pattern[:colors].each_with_index do |color, i|
items["Everlights#{i + 1}_RGB"].update(color)
STYLES.each do |style|
items["Everlights_#{style.capitalize}"].update(pattern[style] || 0)
end
exclusive_styles = pattern.slice(*EXCLUSIVE_STYLES)
exclusive_styles.each do |(style, value)|
items["Everlights_#{style.capitalize}"].update(value)
end
gEverlightsExclusive << 0 if exclusive_styles.empty?
end
EverlightsZone1_Data.update(calculate_data)
EverlightsZone2_Data.update(calculate_data(reverse: true))
2.times { |zone| update_everlights(zone + 1) } if Everlights_Switch.on?
Everlights_PatternChanging.update(OFF)
end
end
def save_pattern(_name)
pattern = {}
pattern[:colors] = Array.new(Everlights_Count.state.to_i) do |i|
items["Everlights#{i + 1}_RGB"].state.to_hex
end
(STYLES + EXCLUSIVE_STYLES).each do |style|
item = items["Everlights_#{style.capitalize}"]
pattern[style] = item.state.to_i if item.state.to_i != 0
end
logger.info("pattern: #{pattern.to_json}")
end
Switch item=Everlights_Switch
Text label="Everlights Pattern" {
Frame label="Presets" {
Selection item=Everlights_Preset mappings=[
""="---",
"Valentine's Day"="Valentine's Day",
"St. Patrick's Day"="St. Patrick's Day",
"Easter"="Easter",
"Patriotic"="Patriotic",
"Halloween"="Halloween",
"Thanksgiving"="Thanksgiving",
"Christmas"="Christmas"]
}
Frame label="Colors" {
Selection item=Everlights_Count mappings=[1="1", 2="2", 3="3", 4="4", 5="5", 6="6", 7="7", 8="8", 9="9", 10="10"]
Colorpicker item=Everlights1_RGB visibility=[Everlights_Count>=1]
Colorpicker item=Everlights2_RGB visibility=[Everlights_Count>=2]
Colorpicker item=Everlights3_RGB visibility=[Everlights_Count>=3]
Colorpicker item=Everlights4_RGB visibility=[Everlights_Count>=4]
Colorpicker item=Everlights5_RGB visibility=[Everlights_Count>=5]
Colorpicker item=Everlights6_RGB visibility=[Everlights_Count>=6]
Colorpicker item=Everlights7_RGB visibility=[Everlights_Count>=7]
Colorpicker item=Everlights8_RGB visibility=[Everlights_Count>=8]
Colorpicker item=Everlights9_RGB visibility=[Everlights_Count>=9]
Colorpicker item=Everlights10_RGB visibility=[Everlights_Count>=10]
}
Frame label="Mode" {
Slider item=Everlights_Twinkle
Slider item=Everlights_Fade
Slider item=Everlights_Blink
Slider item=Everlights_Strobe
Slider item=Everlights_Chase
Slider item=Everlights_ChaseReverse
Slider item=Everlights_Wipe
Slider item=Everlights_Random
}
}
#!/usr/bin/env ruby
require 'cgi'
require 'holidays'
require 'json'
# keys are either a single value, or an array that's a range of two date_specs
# for values, it's the pattern name. if it's nil, match the holiday's name
# for date_specs, it can be a month and day, or it can be a holiday name.
# you can append + or - a number to the end of the string to do lights "around"
# a date
SCHEDULE = {
"Valentine's Day" => nil,
"St. Patrick's Day" => nil,
["Easter Sunday-1", "Easter Sunday"] => "Easter",
"Memorial Day" => "Patriotic",
["Independence Day-3", "Independence Day"] => "Patriotic",
["Jul 24-1", "Jul 24"] => "Patriotic",
["Halloween-7", "Halloween"] => nil,
["Thanksgiving-4", "Thanksgiving"] => nil,
["Thanksgiving+1", "Dec 31"] => "Christmas",
# can't go over the year boundary
"Jan 1" => "Christmas",
}
HOLIDAYS = Holidays.year_holidays([:us, :informal], Date.parse('Jan 1')).map { |x| [x[:name], x[:date]] }.to_h
def normalize_schedule(schedule)
schedule.map do |(dates, pattern)|
dates = [dates, dates] unless dates.is_a?(Array)
pattern ||= dates.last
[dates, pattern]
end.to_h
end
def identify_date(date_spec)
offset = 0
if date_spec =~ /^(.+)([+-]\d+)$/
offset = $2.to_i
date_spec = $1
end
date = HOLIDAYS[date_spec] || Date.parse(date_spec)
date + offset
end
def dates_range(date_specs)
identify_date(date_specs.first)..identify_date(date_specs.last)
end
def search_schedule(date)
normalize_schedule(SCHEDULE).find { |(date_specs, pattern)| dates_range(date_specs).include?(date) }&.last
end
date = ARGV[0] ? Date.parse(ARGV[0]) : Date.today
puts search_schedule(date)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment