everlights 0.1.0 → 0.1.1
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.
- checksums.yaml +4 -4
- data/README.md +6 -1
- data/exe/everlights +24 -1
- data/lib/everlights/sync.rb +14 -4
- data/skills/everlights/SKILL.md +2 -2
- metadata +3 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ee4915d68adebdaaaa5d1ea02be82d7530ebbdee06fbfd2905b73aa49fa9cf6e
|
|
4
|
+
data.tar.gz: f6ed7abeccd0e26b23795679e3cf8dcf876b73163cd684d9ffe109ceadc8a085
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 011d597b5b53676084adad56466ddb9dde0fa77f8e6c6a37264078495a10959e3350a90e0d365afbcf24c4c0a3a0188b0dd3476808a1fc10bf9f63761de0daee
|
|
7
|
+
data.tar.gz: d99fd9e1c63b2d3e70edd0e0a0ce3cc072a677f4a3c4734b65d90bcdeb45662f2f5e1f42f49eea37a9b5fbdbd6b4d59060492a1478aca51dedb7a165cdd6050a
|
data/README.md
CHANGED
|
@@ -26,6 +26,7 @@ Requires Ruby 3.1+.
|
|
|
26
26
|
|
|
27
27
|
```sh
|
|
28
28
|
everlights bridge # firmware, network, cloud, zone health
|
|
29
|
+
everlights bridge health # ok/WARN checks; exits 2 on any warning
|
|
29
30
|
everlights show # what's playing, e.g. "Halloween Fade (Halloween show)"
|
|
30
31
|
|
|
31
32
|
everlights play "Hallows Eve" # a saved sequence, now
|
|
@@ -89,6 +90,8 @@ schedule: # higher in the list wins a shared night
|
|
|
89
90
|
To keep the calendar topped up, run it on a schedule, for example monthly from cron:
|
|
90
91
|
`0 9 1 * * cd ~/everlights && everlights sync >> config/sync.log 2>&1`
|
|
91
92
|
|
|
93
|
+
To only be told when it needs a sync, use `everlights sync --check`. It prints the plan and changes nothing. It exits 0 when the bridge matches the config, 2 when it needs a sync, and 1 on an error.
|
|
94
|
+
|
|
92
95
|
## Ruby
|
|
93
96
|
|
|
94
97
|
```ruby
|
|
@@ -161,7 +164,9 @@ Observed on the bridge (`ruby -Ilib test/live_check.rb` repeats the round trip):
|
|
|
161
164
|
ruby -Ilib -e 'Dir["test/*_test.rb"].each { require "./#{_1}" }'
|
|
162
165
|
```
|
|
163
166
|
|
|
164
|
-
The tests run against a fake bridge, so no hardware is needed.
|
|
167
|
+
The tests run against a fake bridge, so no hardware is needed. CI runs them on every push and pull request.
|
|
168
|
+
|
|
169
|
+
To release, bump the version in `everlights.gemspec`, commit, then tag and push (`git tag v0.2.0 && git push origin v0.2.0`). The release workflow tests, then publishes to RubyGems and GitHub Packages and creates a GitHub release with the `.gem` attached.
|
|
165
170
|
|
|
166
171
|
## Credits
|
|
167
172
|
|
data/exe/everlights
CHANGED
|
@@ -7,6 +7,7 @@ USAGE = <<~TXT
|
|
|
7
7
|
|
|
8
8
|
Bridge:
|
|
9
9
|
bridge firmware, network, cloud, zone health
|
|
10
|
+
bridge health ok/WARN per check; exit 2 on any warning (for cron)
|
|
10
11
|
bridge time [TIME] set the bridge clock (default now)
|
|
11
12
|
bridge reboot --yes restart the bridge
|
|
12
13
|
zones raw zone data (JSON)
|
|
@@ -32,6 +33,7 @@ USAGE = <<~TXT
|
|
|
32
33
|
|
|
33
34
|
Sync (config/everlights.yml: sequences + schedule, see config/everlights.example.yml):
|
|
34
35
|
sync [--dry-run] [--adopt] build sequences and the calendar ahead; prints the plan
|
|
36
|
+
sync --check plan only; exit 2 if the bridge needs a sync (for cron)
|
|
35
37
|
|
|
36
38
|
Options:
|
|
37
39
|
TXT
|
|
@@ -53,6 +55,7 @@ parser = OptionParser.new do |o|
|
|
|
53
55
|
o.on("--json", "JSON output for lists, show and bridge") { opts[:json] = true }
|
|
54
56
|
o.on("--yes", "confirm bridge reboot") { opts[:yes] = true }
|
|
55
57
|
o.on("--dry-run", "sync: print the plan only") { opts[:dry_run] = true }
|
|
58
|
+
o.on("--check", "sync: plan only, exit 2 if anything would change") { opts[:check] = true }
|
|
56
59
|
o.on("--adopt", "sync: take over bridge items whose names match the config") { opts[:adopt] = true }
|
|
57
60
|
end
|
|
58
61
|
parser.parse!
|
|
@@ -70,7 +73,7 @@ begin
|
|
|
70
73
|
zones = opts[:zones]
|
|
71
74
|
cmd = ARGV.shift
|
|
72
75
|
cmd = "#{cmd} #{ARGV.shift}" if %w[sequence calendar].include?(cmd) ||
|
|
73
|
-
(cmd == "bridge" && %w[time reboot].include?(ARGV.first))
|
|
76
|
+
(cmd == "bridge" && %w[health time reboot].include?(ARGV.first))
|
|
74
77
|
arg = ARGV.join(" ")
|
|
75
78
|
|
|
76
79
|
case cmd
|
|
@@ -91,6 +94,25 @@ begin
|
|
|
91
94
|
puts " last request #{at.(z["lastRequestDate"])}, response #{at.(z["lastResponseDate"])}, reboots #{z["rebootCount"]}"
|
|
92
95
|
end
|
|
93
96
|
end
|
|
97
|
+
when "bridge health"
|
|
98
|
+
# ponytail: thresholds are guesses from one bridge; tune when real failures show what matters
|
|
99
|
+
b = el.status
|
|
100
|
+
ago = ->(v) { v ? ((Time.now - Time.parse(v)) / 60).round : Float::INFINITY }
|
|
101
|
+
mins = ->(m) { m.infinite? ? "never" : "#{m} min ago" }
|
|
102
|
+
cloud, radio = ago.(b["lastServerCheckin"]), ago.(b["lastRadioResponse"])
|
|
103
|
+
checks = [
|
|
104
|
+
[!!b["serverReachable"] && cloud <= 15, "cloud", "#{b["serverReachable"] ? "reachable" : "unreachable"}, checked in #{mins.(cloud)}"],
|
|
105
|
+
[!!b.dig("network", "ntp"), "clock", "ntp #{b.dig("network", "ntp") ? "on" : "off"}"],
|
|
106
|
+
[b["minFreeHeap"].to_i >= 10_000, "memory", "lowest free since boot #{b["minFreeHeap"]} bytes (warn under 10000)"],
|
|
107
|
+
[radio <= 10, "radio", "answered #{mins.(radio)}, #{b["radioRebootCount"]} radio reboots"],
|
|
108
|
+
*b["zones"].map do |z|
|
|
109
|
+
last = ago.(z["lastResponseDate"])
|
|
110
|
+
[!!z["active"] && last <= 10, "zone #{z["serial"]}", "#{z["active"] ? "active" : "inactive"}, answered #{mins.(last)}, #{z["rebootCount"]} reboots"]
|
|
111
|
+
end,
|
|
112
|
+
[:info, "reboots", "#{b["rebootCount"]} total, last reset reason #{b["resetReason"]}"]
|
|
113
|
+
]
|
|
114
|
+
checks.each { |ok, name, text| puts "#{{info: "info", true => "ok ", false => "WARN"}[ok]} #{name.ljust(19)} #{text}" }
|
|
115
|
+
exit 2 unless checks.all? { _1[0] }
|
|
94
116
|
when "bridge time"
|
|
95
117
|
time = ARGV.empty? ? Time.now : Time.parse(arg)
|
|
96
118
|
el.set_time(time)
|
|
@@ -179,6 +201,7 @@ begin
|
|
|
179
201
|
plan = Everlights::Sync.new(el, config, state_path: state_path, adopt: opts[:adopt]).plan
|
|
180
202
|
puts plan
|
|
181
203
|
if plan.empty? then nil
|
|
204
|
+
elsif opts[:check] then exit 2
|
|
182
205
|
elsif opts[:dry_run] then puts "\n(dry run: nothing applied)"
|
|
183
206
|
else
|
|
184
207
|
plan.apply!
|
data/lib/everlights/sync.rb
CHANGED
|
@@ -33,21 +33,31 @@ module Everlights
|
|
|
33
33
|
rows.each { |a| out << " #{mark[a.op]} #{a.op.to_s.ljust(7)} #{a.name}#{": #{a.detail}" if a.detail}" }
|
|
34
34
|
end
|
|
35
35
|
lost, other = skips.partition { |_, _, why| why.end_with?(" wins this night") }
|
|
36
|
+
dark, other = other.partition { |_, _, why| why.start_with?("dusk (") }
|
|
36
37
|
unless other.empty?
|
|
37
38
|
out << "Skipped"
|
|
38
39
|
other.each { |d, rule, why| out << " #{d.strftime("%a %Y-%m-%d")} #{rule}: #{why}" }
|
|
39
40
|
end
|
|
41
|
+
unless dark.empty?
|
|
42
|
+
out << "No show: dusk is after the end time"
|
|
43
|
+
dark.group_by { _2 }.each do |rule, rows|
|
|
44
|
+
out << " #{rule}: #{nights(rows.map(&:first))} (dusk up to #{rows.map { _3[/\d\d:\d\d/] }.max}, ends #{rows[0][2][/(\d\d:\d\d) end/, 1]})"
|
|
45
|
+
end
|
|
46
|
+
end
|
|
40
47
|
unless lost.empty?
|
|
41
48
|
out << "Nights taken by a higher rule"
|
|
42
49
|
lost.group_by { |_, rule, why| [rule, why.delete_suffix(" wins this night")] }.each do |(rule, winner), rows|
|
|
43
|
-
|
|
44
|
-
span = days.size == 1 ? days[0].strftime("%b %-d") : "#{days[0].strftime("%b %-d")} – #{days[-1].strftime("%b %-d")}"
|
|
45
|
-
out << " #{rule}: #{days.size} night#{"s" unless days.size == 1} to #{winner} (#{span})"
|
|
50
|
+
out << " #{rule}: #{nights(rows.map(&:first)).sub(", ", " to #{winner} (")})"
|
|
46
51
|
end
|
|
47
52
|
end
|
|
48
53
|
out << "Nothing to change." if empty?
|
|
49
54
|
out.join("\n")
|
|
50
55
|
end
|
|
56
|
+
|
|
57
|
+
def nights(days) # "3 nights, Jun 26 – Jun 28"
|
|
58
|
+
span = days.size == 1 ? days[0].strftime("%b %-d") : "#{days[0].strftime("%b %-d")} – #{days[-1].strftime("%b %-d")}"
|
|
59
|
+
"#{days.size} night#{"s" unless days.size == 1}, #{span}"
|
|
60
|
+
end
|
|
51
61
|
end
|
|
52
62
|
|
|
53
63
|
def self.load_config(path)
|
|
@@ -128,7 +138,7 @@ module Everlights
|
|
|
128
138
|
losers.each { skips << [d, _1[:name], "#{winner[:name]} wins this night"] }
|
|
129
139
|
s, e = window(winner, d)
|
|
130
140
|
if e.nil?
|
|
131
|
-
skips << [d, winner[:name], "dusk (#{s.strftime("%H:%M")}) is after its end
|
|
141
|
+
skips << [d, winner[:name], "dusk (#{s.strftime("%H:%M")}) is after its #{format("%02d:%02d", *winner[:end])} end"]
|
|
132
142
|
elsif s <= @now + MARGIN
|
|
133
143
|
skips << [d, winner[:name], "already started or starts within 15 minutes"]
|
|
134
144
|
elsif (other = others.find { |_, os, oe| os < e && s < oe })
|
data/skills/everlights/SKILL.md
CHANGED
|
@@ -9,7 +9,7 @@ The `everlights` CLI talks to the bridge on the LAN (no auth). It needs `EVERLIG
|
|
|
9
9
|
|
|
10
10
|
## Before you write anything
|
|
11
11
|
|
|
12
|
-
1. Run `everlights calendar list` and `everlights show` (names what's playing and which show it's from) first (`everlights bridge` if something seems broken). Report what's already scheduled or showing.
|
|
12
|
+
1. Run `everlights calendar list` and `everlights show` (names what's playing and which show it's from) first (`everlights bridge health` if something seems broken; it marks problems WARN). Report what's already scheduled or showing.
|
|
13
13
|
2. Confirm before every write: `set`, `play`, `off`, `clear`, `sequence add/update/remove`, `calendar add/remove` and `bridge time`. These change real lights on a real house, so state the exact command you'll run.
|
|
14
14
|
3. Only run `bridge reboot --yes` when the user explicitly asks; it interrupts the lights. The CLI refuses to remove built-in sequences, or sequences an event still uses.
|
|
15
15
|
|
|
@@ -59,7 +59,7 @@ el.calendar.remove "2026-12-24", "Christmas"
|
|
|
59
59
|
When the user wants a recurring or seasonal plan ("Christmas every December", "Fridays in football season"), edit `config/everlights.yml` rather than adding calendar entries one by one. The format reference is `config/everlights.example.yml`.
|
|
60
60
|
|
|
61
61
|
1. Edit `sequences:` or `schedule:`. Rules higher in the list win nights they share.
|
|
62
|
-
2. Run `everlights sync --dry-run` and show the user the plan, including the Skipped
|
|
62
|
+
2. Run `everlights sync --dry-run` and show the user the plan, including the Skipped and No show sections. `everlights sync --check` is the same plan, but exits 2 when anything would change.
|
|
63
63
|
3. Run `everlights sync` only after they confirm.
|
|
64
64
|
|
|
65
65
|
Sync only changes what it created itself. Config problems abort before anything is written; fix every one it lists. Use `--adopt` only when the user says the state file was lost.
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: everlights
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ricky Chilcott
|
|
@@ -31,6 +31,7 @@ metadata:
|
|
|
31
31
|
source_code_uri: https://github.com/rickychilcott/everlights
|
|
32
32
|
bug_tracker_uri: https://github.com/rickychilcott/everlights/issues
|
|
33
33
|
rubygems_mfa_required: 'true'
|
|
34
|
+
github_repo: ssh://github.com/rickychilcott/everlights
|
|
34
35
|
rdoc_options: []
|
|
35
36
|
require_paths:
|
|
36
37
|
- lib
|
|
@@ -45,7 +46,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
45
46
|
- !ruby/object:Gem::Version
|
|
46
47
|
version: '0'
|
|
47
48
|
requirements: []
|
|
48
|
-
rubygems_version:
|
|
49
|
+
rubygems_version: 4.0.20
|
|
49
50
|
specification_version: 4
|
|
50
51
|
summary: Client and CLI for the EverLights Smart Lights Bridge local API
|
|
51
52
|
test_files: []
|