commit c688140c7294c82e20d234082e3c1703949947dd
parent 0b71de1ad9674a821afb5b2cd18b5a938735c296
Author: mtmn <miro@haravara.org>
Date: Sun, 10 May 2026 18:40:56 +0200
hue: fix code smells, add tooling
Diffstat:
52 files changed, 1353 insertions(+), 384 deletions(-)
diff --git a/hue/.gitignore b/hue/.gitignore
@@ -1 +1,9 @@
vendor/
+coverage/
+pkg/
+tmp/
+.rubocop_cache/
+sorbet/rbi/gems/
+sorbet/rbi/dsl/
+sorbet/rbi/annotations/
+sorbet/tapioca/require.rb
diff --git a/hue/.reek.yml b/hue/.reek.yml
@@ -0,0 +1,71 @@
+---
+exclude_paths:
+ - spec
+ - pkg
+ - vendor
+ - sorbet
+ - coverage
+
+detectors:
+ IrresponsibleModule:
+ enabled: false
+ Attribute:
+ enabled: false
+ BooleanParameter:
+ enabled: false
+ DataClump:
+ enabled: false
+ MissingSafeMethod:
+ enabled: false
+ InstanceVariableAssumption:
+ enabled: false
+ TooManyMethods:
+ exclude:
+ - Hue::Bridge
+ - Hue::Bulb
+ TooManyInstanceVariables:
+ exclude:
+ - Hue::Bulb
+ TooManyConstants:
+ exclude:
+ - Hue::Colors::HueSaturation
+ - Hue
+ TooManyStatements:
+ max_statements: 12
+ exclude:
+ - Hue::Colors::HueSaturation#to_rgb
+ - Hue::Colors::RGB#to_hash
+ UncommunicativeVariableName:
+ accept:
+ - _
+ - i
+ - t
+ - h
+ - s
+ - v
+ - x
+ - y
+ - z
+ - p
+ - q
+ - r
+ - g
+ - b
+ - f
+ - d
+ - k
+ - m
+ - a
+ - e
+ UtilityFunction:
+ public_methods_only: true
+ FeatureEnvy:
+ exclude:
+ - Hue::Colors::ColorTemperature#to_rgb
+ - Hue::Colors::HueSaturation#to_rgb
+ - Hue::Colors::RGB#==
+ - Hue::Config::Abstract#==
+ - Hue::Bridge#print_lights
+ - Hue::Bridge#print_proxy_info
+ - Hue::Bridge#receive
+ - Hue::Bridge#send_request
diff --git a/hue/.rspec b/hue/.rspec
@@ -0,0 +1,2 @@
+--exclude-pattern "vendor/**/*_spec.rb,pkg/**/*_spec.rb"
+--require spec_helper
diff --git a/hue/.rubocop.yml b/hue/.rubocop.yml
@@ -0,0 +1,105 @@
+plugins:
+ - rubocop-rake
+ - rubocop-rspec
+
+AllCops:
+ TargetRubyVersion: 3.0
+ NewCops: enable
+ SuggestExtensions: false
+ Exclude:
+ - 'pkg/**/*'
+ - 'vendor/**/*'
+ - 'sorbet/**/*'
+ - 'tmp/**/*'
+ - 'coverage/**/*'
+ - 'hue-*.gem'
+
+Style/Documentation:
+ Enabled: false
+
+Style/StringLiterals:
+ EnforcedStyle: single_quotes
+
+Metrics/BlockLength:
+ Exclude:
+ - 'spec/**/*'
+ - 'Rakefile'
+ - '*.gemspec'
+
+Metrics/MethodLength:
+ Max: 20
+ Exclude:
+ - 'lib/hue/colors/hue_saturation.rb'
+ - 'lib/hue/colors/rgb.rb'
+
+Layout/LineLength:
+ Max: 120
+
+# Color math methods are inherently dense; the public CLI verbs Bulb#on/#off,
+# Hue.remove_default look like predicates but aren't.
+Metrics/AbcSize:
+ Exclude:
+ - 'lib/hue/animations/candle.rb'
+ - 'lib/hue/colors/color_temperature.rb'
+ - 'lib/hue/colors/hue_saturation.rb'
+ - 'lib/hue/colors/rgb.rb'
+
+Metrics/CyclomaticComplexity:
+ Exclude:
+ - 'lib/hue/colors/hue_saturation.rb'
+
+Metrics/ClassLength:
+ Exclude:
+ - 'lib/hue/bulb.rb'
+
+Metrics/ModuleLength:
+ Exclude:
+ - 'lib/hue.rb'
+ - 'spec/test_helpers.rb'
+
+Metrics/ParameterLists:
+ CountKeywordArgs: false
+ Max: 5
+ MaxOptionalParameters: 4
+
+Naming/PredicateMethod:
+ Enabled: false
+
+Style/MultilineBlockChain:
+ Enabled: false
+
+Style/OptionalBooleanParameter:
+ Exclude:
+ - 'spec/test_helpers.rb'
+
+# Specs verify behavior via webmock stubs and side effects; explicit expects
+# are not always present, and the project keeps a few stylistic rules relaxed.
+RSpec/ExampleLength:
+ Max: 15
+
+RSpec/MultipleExpectations:
+ Max: 10
+
+RSpec/NoExpectationExample:
+ Enabled: false
+
+RSpec/MessageSpies:
+ Enabled: false
+
+RSpec/InstanceVariable:
+ Enabled: false
+
+RSpec/ContextWording:
+ Enabled: false
+
+RSpec/SpecFilePathFormat:
+ Enabled: false
+
+RSpec/MultipleMemoizedHelpers:
+ Enabled: false
+
+RSpec/BeforeAfterAll:
+ Enabled: false
+
+RSpec/DescribeMethod:
+ Enabled: false
diff --git a/hue/Gemfile b/hue/Gemfile
@@ -3,3 +3,24 @@
source 'https://rubygems.org'
gemspec
+
+group :development do
+ gem 'debug', '>= 1.0'
+ gem 'rake'
+ gem 'rspec', '>= 3.0'
+ gem 'ruby-lsp'
+ gem 'webmock', '>= 3.0'
+
+ gem 'reek'
+ gem 'rubocop'
+ gem 'rubocop-rake'
+ gem 'rubocop-rspec'
+
+ gem 'rbs'
+ gem 'sorbet'
+ gem 'sorbet-runtime'
+ gem 'tapioca'
+
+ gem 'bundler-audit'
+ gem 'simplecov'
+end
diff --git a/hue/Gemfile.lock b/hue/Gemfile.lock
@@ -1,7 +1,7 @@
PATH
remote: .
specs:
- hue (0.4.0)
+ hue (1.0.0)
json (~> 2.18)
matrix
@@ -10,16 +10,102 @@ GEM
specs:
addressable (2.9.0)
public_suffix (>= 2.0.2, < 8.0)
+ ast (2.4.3)
+ benchmark (0.5.0)
bigdecimal (4.1.2)
+ bundler-audit (0.9.3)
+ bundler (>= 1.2.0)
+ thor (~> 1.0)
+ concurrent-ruby (1.3.6)
crack (1.0.1)
bigdecimal
rexml
+ date (3.5.1)
+ debug (1.11.1)
+ irb (~> 1.10)
+ reline (>= 0.3.8)
diff-lcs (1.6.2)
+ docile (1.4.1)
+ dry-configurable (1.3.0)
+ dry-core (~> 1.1)
+ zeitwerk (~> 2.6)
+ dry-core (1.2.0)
+ concurrent-ruby (~> 1.0)
+ logger
+ zeitwerk (~> 2.6)
+ dry-inflector (1.3.1)
+ dry-initializer (3.2.0)
+ dry-logic (1.6.0)
+ bigdecimal
+ concurrent-ruby (~> 1.0)
+ dry-core (~> 1.1)
+ zeitwerk (~> 2.6)
+ dry-schema (1.16.0)
+ concurrent-ruby (~> 1.0)
+ dry-configurable (~> 1.0, >= 1.0.1)
+ dry-core (~> 1.1)
+ dry-initializer (~> 3.2)
+ dry-logic (~> 1.6)
+ dry-types (~> 1.9, >= 1.9.1)
+ zeitwerk (~> 2.6)
+ dry-types (1.9.1)
+ bigdecimal (>= 3.0)
+ concurrent-ruby (~> 1.0)
+ dry-core (~> 1.0)
+ dry-inflector (~> 1.0)
+ dry-logic (~> 1.4)
+ zeitwerk (~> 2.6)
+ erb (6.0.4)
+ erubi (1.13.1)
hashdiff (1.2.1)
+ io-console (0.8.2)
+ irb (1.18.0)
+ pp (>= 0.6.0)
+ prism (>= 1.3.0)
+ rdoc (>= 4.0.0)
+ reline (>= 0.4.2)
json (2.19.4)
+ language_server-protocol (3.17.0.5)
+ lint_roller (1.1.0)
+ logger (1.7.0)
matrix (0.4.3)
+ netrc (0.11.0)
+ parallel (2.1.0)
+ parser (3.3.11.1)
+ ast (~> 2.4.1)
+ racc
+ pp (0.6.3)
+ prettyprint
+ prettyprint (0.2.0)
+ prism (1.9.0)
+ psych (5.3.1)
+ date
+ stringio
public_suffix (7.0.5)
+ racc (1.8.1)
+ rainbow (3.1.1)
rake (13.4.2)
+ rbi (0.3.11)
+ prism (~> 1.0)
+ rbs (>= 4.0.1)
+ rbs (4.0.2)
+ logger
+ prism (>= 1.6.0)
+ tsort
+ rdoc (7.2.0)
+ erb
+ psych (>= 4.0.0)
+ tsort
+ reek (6.5.0)
+ dry-schema (~> 1.13)
+ logger (~> 1.6)
+ parser (~> 3.3.0)
+ rainbow (>= 2.0, < 4.0)
+ rexml (~> 3.1)
+ regexp_parser (2.12.0)
+ reline (0.6.3)
+ io-console (~> 0.5)
+ require-hooks (0.4.0)
rexml (3.4.4)
rspec (3.13.2)
rspec-core (~> 3.13.0)
@@ -34,39 +120,176 @@ GEM
diff-lcs (>= 1.2.0, < 2.0)
rspec-support (~> 3.13.0)
rspec-support (3.13.7)
+ rubocop (1.86.1)
+ json (~> 2.3)
+ language_server-protocol (~> 3.17.0.2)
+ lint_roller (~> 1.1.0)
+ parallel (>= 1.10)
+ parser (>= 3.3.0.2)
+ rainbow (>= 2.2.2, < 4.0)
+ regexp_parser (>= 2.9.3, < 3.0)
+ rubocop-ast (>= 1.49.0, < 2.0)
+ ruby-progressbar (~> 1.7)
+ unicode-display_width (>= 2.4.0, < 4.0)
+ rubocop-ast (1.49.1)
+ parser (>= 3.3.7.2)
+ prism (~> 1.7)
+ rubocop-rake (0.7.1)
+ lint_roller (~> 1.1)
+ rubocop (>= 1.72.1)
+ rubocop-rspec (3.9.0)
+ lint_roller (~> 1.1)
+ rubocop (~> 1.81)
+ ruby-lsp (0.26.9)
+ language_server-protocol (~> 3.17.0)
+ prism (>= 1.2, < 2.0)
+ rbs (>= 3, < 5)
+ ruby-progressbar (1.13.0)
+ rubydex (0.2.2-x86_64-linux)
+ simplecov (0.22.0)
+ docile (~> 1.1)
+ simplecov-html (~> 0.11)
+ simplecov_json_formatter (~> 0.1)
+ simplecov-html (0.13.2)
+ simplecov_json_formatter (0.1.4)
+ sorbet (0.6.13208)
+ sorbet-static (= 0.6.13208)
+ sorbet-runtime (0.6.13208)
+ sorbet-static (0.6.13208-x86_64-linux)
+ sorbet-static-and-runtime (0.6.13208)
+ sorbet (= 0.6.13208)
+ sorbet-runtime (= 0.6.13208)
+ spoom (1.7.14)
+ erubi (>= 1.10.0)
+ prism (>= 0.28.0)
+ rbi (>= 0.3.3)
+ rbs (>= 4.0.0.dev.5)
+ rexml (>= 3.2.6)
+ sorbet-static-and-runtime (>= 0.5.10187)
+ thor (>= 0.19.2)
+ stringio (3.2.0)
+ tapioca (0.19.1)
+ benchmark
+ bundler (>= 2.2.25)
+ netrc (>= 0.11.0)
+ parallel (>= 1.21.0)
+ rbi (>= 0.3.7)
+ require-hooks (>= 0.2.2)
+ rubydex (>= 0.1.0.beta10)
+ sorbet-static-and-runtime (>= 0.6.12698)
+ spoom (>= 1.7.9)
+ thor (>= 1.2.0)
+ tsort
+ thor (1.5.0)
+ tsort (0.2.0)
+ unicode-display_width (3.2.0)
+ unicode-emoji (~> 4.1)
+ unicode-emoji (4.2.0)
webmock (3.26.2)
addressable (>= 2.8.0)
crack (>= 0.3.2)
hashdiff (>= 0.4.0, < 2.0.0)
+ zeitwerk (2.7.5)
PLATFORMS
- ruby
x86_64-linux
DEPENDENCIES
+ bundler-audit
+ debug (>= 1.0)
hue!
rake
+ rbs
+ reek
rspec (>= 3.0)
+ rubocop
+ rubocop-rake
+ rubocop-rspec
+ ruby-lsp
+ simplecov
+ sorbet
+ sorbet-runtime
+ tapioca
webmock (>= 3.0)
CHECKSUMS
addressable (2.9.0) sha256=7fdf6ac3660f7f4e867a0838be3f6cf722ace541dd97767fa42bc6cfa980c7af
+ ast (2.4.3) sha256=954615157c1d6a382bc27d690d973195e79db7f55e9765ac7c481c60bdb4d383
+ benchmark (0.5.0) sha256=465df122341aedcb81a2a24b4d3bd19b6c67c1530713fd533f3ff034e419236c
bigdecimal (4.1.2) sha256=53d217666027eab4280346fba98e7d5b66baaae1b9c3c1c0ffe89d48188a3fbd
+ bundler (4.0.11) sha256=5bcec0fb78302e48d02ee46f10ee6e6942be647ba5b44a6d1ddfda9a240ce785
+ bundler-audit (0.9.3) sha256=81c8766c71e47d0d28a0f98c7eed028539f21a6ea3cd8f685eb6f42333c9b4e9
+ concurrent-ruby (1.3.6) sha256=6b56837e1e7e5292f9864f34b69c5a2cbc75c0cf5338f1ce9903d10fa762d5ab
crack (1.0.1) sha256=ff4a10390cd31d66440b7524eb1841874db86201d5b70032028553130b6d4c7e
+ date (3.5.1) sha256=750d06384d7b9c15d562c76291407d89e368dda4d4fff957eb94962d325a0dc0
+ debug (1.11.1) sha256=2e0b0ac6119f2207a6f8ac7d4a73ca8eb4e440f64da0a3136c30343146e952b6
diff-lcs (1.6.2) sha256=9ae0d2cba7d4df3075fe8cd8602a8604993efc0dfa934cff568969efb1909962
+ docile (1.4.1) sha256=96159be799bfa73cdb721b840e9802126e4e03dfc26863db73647204c727f21e
+ dry-configurable (1.3.0) sha256=882d862858567fc1210d2549d4c090f34370fc1bb7c5c1933de3fe792e18afa8
+ dry-core (1.2.0) sha256=0cc5a7da88df397f153947eeeae42e876e999c1e30900f3c536fb173854e96a1
+ dry-inflector (1.3.1) sha256=7fb0c2bb04f67638f25c52e7ba39ab435d922a3a5c3cd196120f63accb682dcc
+ dry-initializer (3.2.0) sha256=37d59798f912dc0a1efe14a4db4a9306989007b302dcd5f25d0a2a20c166c4e3
+ dry-logic (1.6.0) sha256=da6fedbc0f90fc41f9b0cc7e6f05f5d529d1efaef6c8dcc8e0733f685745cea2
+ dry-schema (1.16.0) sha256=cd3aaeabc0f1af66ec82a29096d4c4fb92a0a58b9dae29a22b1bbceb78985727
+ dry-types (1.9.1) sha256=baebeecdb9f8395d6c9d227b62011279440943e3ef2468fe8ccc1ba11467f178
+ erb (6.0.4) sha256=38e3803694be357fe2bfe312487c74beaf9fb4e5beb3e22498952fe1645b95d9
+ erubi (1.13.1) sha256=a082103b0885dbc5ecf1172fede897f9ebdb745a4b97a5e8dc63953db1ee4ad9
hashdiff (1.2.1) sha256=9c079dbc513dfc8833ab59c0c2d8f230fa28499cc5efb4b8dd276cf931457cd1
- hue (0.4.0)
+ hue (1.0.0)
+ io-console (0.8.2) sha256=d6e3ae7a7cc7574f4b8893b4fca2162e57a825b223a177b7afa236c5ef9814cc
+ irb (1.18.0) sha256=de9454a0703a54704b9811a5ef31a60c86949fbf4013fcf244fabc7c775248e3
json (2.19.4) sha256=670a7d333fb3b18ca5b29cb255eb7bef099e40d88c02c80bd42a3f30fe5239ac
+ language_server-protocol (3.17.0.5) sha256=fd1e39a51a28bf3eec959379985a72e296e9f9acfce46f6a79d31ca8760803cc
+ lint_roller (1.1.0) sha256=2c0c845b632a7d172cb849cc90c1bce937a28c5c8ccccb50dfd46a485003cc87
+ logger (1.7.0) sha256=196edec7cc44b66cfb40f9755ce11b392f21f7967696af15d274dde7edff0203
matrix (0.4.3) sha256=a0d5ab7ddcc1973ff690ab361b67f359acbb16958d1dc072b8b956a286564c5b
+ netrc (0.11.0) sha256=de1ce33da8c99ab1d97871726cba75151113f117146becbe45aa85cb3dabee3f
+ parallel (2.1.0) sha256=b35258865c2e31134c5ecb708beaaf6772adf9d5efae28e93e99260877b09356
+ parser (3.3.11.1) sha256=d17ace7aabe3e72c3cc94043714be27cc6f852f104d81aa284c2281aecc65d54
+ pp (0.6.3) sha256=2951d514450b93ccfeb1df7d021cae0da16e0a7f95ee1e2273719669d0ab9df6
+ prettyprint (0.2.0) sha256=2bc9e15581a94742064a3cc8b0fb9d45aae3d03a1baa6ef80922627a0766f193
+ prism (1.9.0) sha256=7b530c6a9f92c24300014919c9dcbc055bf4cdf51ec30aed099b06cd6674ef85
+ psych (5.3.1) sha256=eb7a57cef10c9d70173ff74e739d843ac3b2c019a003de48447b2963d81b1974
public_suffix (7.0.5) sha256=1a8bb08f1bbea19228d3bed6e5ed908d1cb4f7c2726d18bd9cadf60bc676f623
+ racc (1.8.1) sha256=4a7f6929691dbec8b5209a0b373bc2614882b55fc5d2e447a21aaa691303d62f
+ rainbow (3.1.1) sha256=039491aa3a89f42efa1d6dec2fc4e62ede96eb6acd95e52f1ad581182b79bc6a
rake (13.4.2) sha256=cb825b2bd5f1f8e91ca37bddb4b9aaf345551b4731da62949be002fa89283701
+ rbi (0.3.11) sha256=d8f0f1b45f8b470dc3bdd1e8192b1d554c826d66a4fb73e68480c4230c638d02
+ rbs (4.0.2) sha256=af75671e66cd03434cc546622741ebf83f6197ec4328375805306330bf78ef25
+ rdoc (7.2.0) sha256=8650f76cd4009c3b54955eb5d7e3a075c60a57276766ebf36f9085e8c9f23192
+ reek (6.5.0) sha256=d26d3a492773b2bbc228888067a21afe33ac07954a17dbd64cdeae42c4c69be1
+ regexp_parser (2.12.0) sha256=35a916a1d63190ab5c9009457136ae5f3c0c7512d60291d0d1378ba18ce08ebb
+ reline (0.6.3) sha256=1198b04973565b36ec0f11542ab3f5cfeeec34823f4e54cebde90968092b1835
+ require-hooks (0.4.0) sha256=005f4c6435b4edae73e358cdbaba48370a4121f9ce893d5d2a3c66fce855677d
rexml (3.4.4) sha256=19e0a2c3425dfbf2d4fc1189747bdb2f849b6c5e74180401b15734bc97b5d142
rspec (3.13.2) sha256=206284a08ad798e61f86d7ca3e376718d52c0bc944626b2349266f239f820587
rspec-core (3.13.6) sha256=a8823c6411667b60a8bca135364351dda34cd55e44ff94c4be4633b37d828b2d
rspec-expectations (3.13.5) sha256=33a4d3a1d95060aea4c94e9f237030a8f9eae5615e9bd85718fe3a09e4b58836
rspec-mocks (3.13.8) sha256=086ad3d3d17533f4237643de0b5c42f04b66348c28bf6b9c2d3f4a3b01af1d47
rspec-support (3.13.7) sha256=0640e5570872aafefd79867901deeeeb40b0c9875a36b983d85f54fb7381c47c
+ rubocop (1.86.1) sha256=44415f3f01d01a21e01132248d2fd0867572475b566ca188a0a42133a08d4531
+ rubocop-ast (1.49.1) sha256=4412f3ee70f6fe4546cc489548e0f6fcf76cafcfa80fa03af67098ffed755035
+ rubocop-rake (0.7.1) sha256=3797f2b6810c3e9df7376c26d5f44f3475eda59eb1adc38e6f62ecf027cbae4d
+ rubocop-rspec (3.9.0) sha256=8fa70a3619408237d789aeecfb9beef40576acc855173e60939d63332fdb55e2
+ ruby-lsp (0.26.9) sha256=33a01c001c00a76b4e821efc04ed7572983430f31ca5d6f3e343d0b6ccab4129
+ ruby-progressbar (1.13.0) sha256=80fc9c47a9b640d6834e0dc7b3c94c9df37f08cb072b7761e4a71e22cff29b33
+ rubydex (0.2.2-x86_64-linux) sha256=b356fbd11401522dc6967637b3c0485827053e549303229eeb8aa669638b0885
+ simplecov (0.22.0) sha256=fe2622c7834ff23b98066bb0a854284b2729a569ac659f82621fc22ef36213a5
+ simplecov-html (0.13.2) sha256=bd0b8e54e7c2d7685927e8d6286466359b6f16b18cb0df47b508e8d73c777246
+ simplecov_json_formatter (0.1.4) sha256=529418fbe8de1713ac2b2d612aa3daa56d316975d307244399fa4838c601b428
+ sorbet (0.6.13208) sha256=040b8de0b8746a2c7a0cabaab6f38b8508c215f344372bf818245986a9ee7240
+ sorbet-runtime (0.6.13208) sha256=2ae9a6e52bbfdd39916448b09bfb165357190effb28fd8c554c5dd2c52be0a32
+ sorbet-static (0.6.13208-x86_64-linux) sha256=bc210d5256c877106c21643f8d5fa8c5b445d5e1a825c0b3b1d0030aed128b09
+ sorbet-static-and-runtime (0.6.13208) sha256=31aa780d04cc6abf39f062cd334bcd243ac8566107ca37440883cac82bae88b7
+ spoom (1.7.14) sha256=48da4e39d9a63611aa0132a3656d7e75833fcccb6006d59f7fa879b98c8877c4
+ stringio (3.2.0) sha256=c37cb2e58b4ffbd33fe5cd948c05934af997b36e0b6ca6fdf43afa234cf222e1
+ tapioca (0.19.1) sha256=5de94d458950897ffe3d4e86a21eec48bb6a8775af85f80b1486b7ae7ba51823
+ thor (1.5.0) sha256=e3a9e55fe857e44859ce104a84675ab6e8cd59c650a49106a05f55f136425e73
+ tsort (0.2.0) sha256=9650a793f6859a43b6641671278f79cfead60ac714148aabe4e3f0060480089f
+ unicode-display_width (3.2.0) sha256=0cdd96b5681a5949cdbc2c55e7b420facae74c4aaf9a9815eee1087cb1853c42
+ unicode-emoji (4.2.0) sha256=519e69150f75652e40bf736106cfbc8f0f73aa3fb6a65afe62fefa7f80b0f80f
webmock (3.26.2) sha256=774556f2ea6371846cca68c01769b2eac0d134492d21f6d0ab5dd643965a4c90
+ zeitwerk (2.7.5) sha256=d8da92128c09ea6ec62c949011b00ed4a20242b255293dd66bf41545398f73dd
BUNDLED WITH
- 4.0.7
+ 4.0.11
diff --git a/hue/Rakefile b/hue/Rakefile
@@ -42,4 +42,23 @@ task :clean do
rm_rf 'pkg'
end
-task default: [:spec]
+require 'rubocop/rake_task'
+RuboCop::RakeTask.new
+
+require 'reek/rake/task'
+Reek::Rake::Task.new
+
+desc 'Run bundler-audit'
+task :audit do
+ sh 'bundle exec bundle-audit check --update'
+end
+
+desc 'Run sorbet typecheck'
+task :sorbet do
+ sh 'bundle exec srb tc'
+end
+
+desc 'Run all static analysis'
+task lint: %i[rubocop reek sorbet audit]
+
+task default: %i[spec rubocop]
diff --git a/hue/hue-0.4.0.gem b/hue/hue-0.4.0.gem
Binary files differ.
diff --git a/hue/hue.gemspec b/hue/hue.gemspec
@@ -4,10 +4,11 @@ $LOAD_PATH.unshift File.expand_path('lib', __dir__)
Gem::Specification.new do |s|
s.name = 'hue'
- s.version = '0.4.0'
+ s.version = '1.0.0'
s.authors = ['mtmn']
- s.email = ['miro@mtmn.name']
- s.homepage = 'https://codeberg.org/mtmn/tools'
+ s.email = ['miro@haravara.org']
+ s.homepage = 'https://git.sr.ht/~mtmn/tools'
+ s.licenses = ['MIT']
s.summary = 'Ruby library and CLI for controlling Philips Hue lights and bridge.'
s.description = 'Library and command line interface for controlling the Philips Hue system. ' \
'Allows registration and invocation of a registered Philips Hue app, with ' \
@@ -21,9 +22,5 @@ Gem::Specification.new do |s|
s.add_dependency('json', '~> 2.18')
s.add_dependency('matrix')
- s.add_development_dependency('rake')
- s.add_development_dependency('rspec', '>= 3.0')
- s.add_development_dependency('webmock', '>= 3.0')
-
s.required_ruby_version = '>= 3.0'
end
diff --git a/hue/lib/hue.rb b/hue/lib/hue.rb
@@ -39,10 +39,10 @@ module Hue
def self.application
application_config = Hue::Config::Application.default
- bridge_config = Hue::Config::Bridge.find(application_config.bridge_id)
- bridge_config ||= register_bridges[application_config.bridge_id]
+ bridge_id = application_config.bridge_id
+ bridge_config = Hue::Config::Bridge.find(bridge_id) || register_bridges[bridge_id]
- raise Error, "Unable to find bridge: #{application_config.bridge_id}" if bridge_config.nil?
+ raise Error, "Unable to find bridge: #{bridge_id}" unless bridge_config
Hue::Bridge.new(application_config.id, bridge_config.uri)
end
@@ -61,39 +61,41 @@ module Hue
bridges
end
- def self.udp_discover(bridges)
- Hue.logger.info('Bridge UDP Discovery')
- payload = <<~PAYLOAD
- M-SEARCH * HTTP/1.1
- ST: ssdp:all
- MX: 10
- MAN: ssdp:discover
- HOST: 239.255.255.250:1900
- PAYLOAD
- broadcast_address = '239.255.255.250'
- port_number = 1900
+ UDP_SSDP_PAYLOAD = <<~PAYLOAD
+ M-SEARCH * HTTP/1.1
+ ST: ssdp:all
+ MX: 10
+ MAN: ssdp:discover
+ HOST: 239.255.255.250:1900
+ PAYLOAD
+ UDP_SSDP_ADDRESS = '239.255.255.250'
+ UDP_SSDP_PORT = 1900
+ def self.udp_discover(bridges)
+ log = Hue.logger
+ log.info('Bridge UDP Discovery')
socket = UDPSocket.new(Socket::AF_INET)
- socket.send(payload, 0, broadcast_address, port_number)
-
- Timeout.timeout(DEFAULT_UDP_TIMEOUT, Hue::Error) do
- loop do
- message, (_, port, hostname, ip_add) = socket.recvfrom(1024)
- if message =~ /IpBridge/ && /LOCATION: (.*)$/.match(message)
- if (uuid_match = /uuid:(.{36})/.match(message))
- # Assume this is Philips Hue for now.
- uuid = uuid_match.captures.first
- logger.info("Found bridge (#{hostname}:#{port}) with uuid: #{uuid}") if bridges[uuid].nil?
- bridges[uuid] = "http://#{ip_add}/api"
- end
- else
- logger.debug("Found #{hostname}:#{port}: #{message}")
- end
+ socket.send(UDP_SSDP_PAYLOAD, 0, UDP_SSDP_ADDRESS, UDP_SSDP_PORT)
+ Timeout.timeout(DEFAULT_UDP_TIMEOUT, Hue::Error) { listen_for_bridges(socket, bridges, log) }
+ rescue Hue::Error => e
+ log.warn(e)
+ log.info('UDPSocket timed out.')
+ end
+
+ def self.listen_for_bridges(socket, bridges, log)
+ loop do
+ message, (_, port, hostname, ip_add) = socket.recvfrom(1024)
+ if message =~ /IpBridge/ && /LOCATION: (.*)$/.match(message)
+ uuid_match = /uuid:(.{36})/.match(message)
+ next unless uuid_match
+
+ uuid = uuid_match.captures.first
+ log.info("Found bridge (#{hostname}:#{port}) with uuid: #{uuid}") unless bridges[uuid]
+ bridges[uuid] = "http://#{ip_add}/api"
+ else
+ log.debug("Found #{hostname}:#{port}: #{message}")
end
end
- rescue Hue::Error => e
- Hue.logger.warn(e)
- logger.info('UDPSocket timed out.')
end
def self.nupnp_discover(bridges)
@@ -106,12 +108,12 @@ module Hue
rescue StandardError
nil
end
- return if json.nil?
+ return unless json
json.each do |bridge|
uuid = bridge['id']
ip_add = bridge['internalipaddress']
- bridges[uuid] = "http://#{ip_add}/api" unless uuid.nil? || ip_add.nil?
+ bridges[uuid] = "http://#{ip_add}/api" if uuid && ip_add
end
end
@@ -135,11 +137,7 @@ module Hue
end
def to_s
- if @original_error.nil?
- super
- else
- "#{super}\nCause: #{@original_error}"
- end
+ @original_error ? "#{super}\nCause: #{@original_error}" : super
end
end
@@ -155,14 +153,7 @@ module Hue
def self.logger
@logger ||= begin
- log_dir_path = File.join(ENV['HOME'], '.hue')
- begin
- FileUtils.mkdir_p(log_dir_path)
- rescue Errno::EACCES
- log_dir_path = File.join(ENV['HOME'], ".#{device_type}")
- FileUtils.mkdir_p(log_dir_path)
- end
-
+ log_dir_path = ensure_log_dir
log_file_path = File.join(log_dir_path, 'hue.log')
log_file = File.new(log_file_path, File::WRONLY | File::APPEND | File::CREAT)
logger = Logger.new(log_file)
@@ -171,6 +162,17 @@ module Hue
end
end
+ def self.ensure_log_dir
+ home = Dir.home
+ [File.join(home, '.hue'), File.join(home, ".#{device_type}")].each do |path|
+ FileUtils.mkdir_p(path)
+ return path
+ rescue Errno::EACCES
+ next
+ end
+ raise Error, 'Unable to create log directory'
+ end
+
def self.percent_to_unit_interval(value)
if (percent = /(\d+)%/.match(value.to_s))
percent.captures.first.to_i / 100.0
diff --git a/hue/lib/hue/animations/sunrise.rb b/hue/lib/hue/animations/sunrise.rb
@@ -13,31 +13,34 @@ module Hue
def perform_sunrise(total_time_in_minutes = 18)
multiplier = sunrise_multiplier total_time_in_minutes
steps = sunrise_steps(multiplier)
- if on?
- puts "ON! #{steps[0][:bri]} :: #{brightness} :: #{brightness > steps[0][:bri]}"
- steps.shift while brightness >= steps[0][:bri]
- end
- steps.each_with_index do |step, _i|
- update step.merge(on: true)
- sleep(step[:transitiontime] / 10.0)
- end
+ skip_until_brightness(steps, :>=)
+ run_steps(steps)
end
def perform_sunset(total_time_in_minutes = 18)
multiplier = sunrise_multiplier total_time_in_minutes
steps = sunset_steps(multiplier)
- if on?
- puts "ON! #{steps[0][:bri]} :: #{brightness} :: #{brightness > steps[0][:bri]}"
- steps.shift while brightness <= steps[0][:bri]
- end
- steps.each_with_index do |step, _i|
+ skip_until_brightness(steps, :<=)
+ run_steps(steps)
+ off
+ end
+
+ private
+
+ def run_steps(steps)
+ steps.each do |step|
update step.merge(on: true)
sleep(step[:transitiontime] / 10.0)
end
- off
end
- private
+ # :reek:DuplicateMethodCall
+ def skip_until_brightness(steps, comparison)
+ return unless on?
+
+ puts "ON! #{steps[0][:bri]} :: #{brightness} :: #{brightness > steps[0][:bri]}"
+ steps.shift while brightness.public_send(comparison, steps[0][:bri])
+ end
def sunrise_multiplier(total_time_in_minutes)
# total_time / 18 steps == time_per_step
diff --git a/hue/lib/hue/bridge.rb b/hue/lib/hue/bridge.rb
@@ -60,9 +60,10 @@ module Hue
def register
response = create(URI.parse(bridge_uri), { 'devicetype' => "#{Hue.device_type}@#{Socket.gethostname}" })
- return unless response.first.key?('success')
+ first = response.first
+ return unless first.key?('success')
- @application_id = response.first['success']['username']
+ @application_id = first['success']['username']
end
def unregister
@@ -104,32 +105,36 @@ module Hue
end
def receive(request_class, url, payload = nil)
- request = request_class.new(url.request_uri, { 'Content-Type' => 'application/json' })
- request.body = payload if payload
- Hue.logger.info("Sending #{payload} to #{url}")
- response = nil
- begin
- http = Net::HTTP.new(url.host, url.port)
- http.open_timeout = 3 # Quick timeout on connection fail.
- http.read_timeout = 8 # Slower timeout on read fail, but way faster than the default.
- response = http.start { |http| http.request(request) }
- rescue StandardError => e
- Hue.logger.error(e.message)
- raise Hue::Error.new('Problem reaching bridge.', e)
- end
+ log = Hue.logger
+ log.info("Sending #{payload} to #{url}")
- if response && response.code.to_s != '200'
- Hue.logger.info("Error with response #{response.code} #{response.message}")
- raise Hue::Error, "Unexpected response: #{response.code}, #{response.message}"
- else
- json = JSON.parse(response.body)
- Hue.logger.info("Response #{response.code} #{response.message}: #{json}")
- if json.is_a?(Array) && (error = json.first['error'])
- raise Hue::API::Error, error
- else
- json
- end
+ response = send_request(request_class, url, payload)
+ code = response.code.to_s
+ message = response.message
+
+ if code != '200'
+ log.info("Error with response #{code} #{message}")
+ raise Hue::Error, "Unexpected response: #{code}, #{message}"
end
+
+ json = JSON.parse(response.body)
+ log.info("Response #{code} #{message}: #{json}")
+ first_error = json.first['error'] if json.is_a?(Array)
+ raise Hue::API::Error, first_error if first_error
+
+ json
+ end
+
+ def send_request(request_class, url, payload)
+ request = request_class.new(url.request_uri, { 'Content-Type' => 'application/json' })
+ request.body = payload if payload
+ http = Net::HTTP.new(url.host, url.port)
+ http.open_timeout = 3
+ http.read_timeout = 8
+ http.start { |conn| conn.request(request) }
+ rescue StandardError => e
+ Hue.logger.error(e.message)
+ raise Hue::Error.new('Problem reaching bridge.', e)
end
end
end
diff --git a/hue/lib/hue/cli.rb b/hue/lib/hue/cli.rb
@@ -48,7 +48,8 @@ module Hue
end
def handle_bridge_error(error, original_args)
- return puts error.message unless ERROR_BRIDGE_CONNECTION_PROBLEM.match?(error.message)
+ message = error.message
+ return puts message unless ERROR_BRIDGE_CONNECTION_PROBLEM.match?(message)
@retry_count += 1
diff --git a/hue/lib/hue/cli/commands/delete.rb b/hue/lib/hue/cli/commands/delete.rb
@@ -6,12 +6,8 @@ module Hue
class Delete < Hue::CLI::Command
def execute(*args)
app_id = args.shift
-
- if app_id.nil?
- bridge.unregister
- else
- Bridge.new(app_id, bridge.bridge_uri).unregister
- end
+ target = app_id ? Bridge.new(app_id, bridge.bridge_uri) : bridge
+ target.unregister
end
protected
diff --git a/hue/lib/hue/cli/commands/light.rb b/hue/lib/hue/cli/commands/light.rb
@@ -70,15 +70,9 @@ module Hue
end
def clear(*args)
- case args.first
- when 'effect'
- bulb.clear_effect
- when 'action'
- bulb.solid
- else
- bulb.clear_effect
- bulb.solid
- end
+ target = args.first
+ bulb.clear_effect if target != 'action'
+ bulb.solid if target != 'effect'
end
def dump(*_args)
diff --git a/hue/lib/hue/cli/processors/light_state_alias.rb b/hue/lib/hue/cli/processors/light_state_alias.rb
@@ -9,8 +9,8 @@ module Hue
ALIAS_FILE_NAME = 'aliases.yml'
ALIAS_FILE_PATHS = [
File.join(Hue::CLI::LOCATION, 'lib', 'config'), # Default (bundled with gem)
- '/etc/hue/', # System
- File.join(ENV['HOME'], '.hue') # User
+ '/etc/hue/', # System
+ File.join(Dir.home, '.hue') # User
].freeze
@alias_map = {}
@@ -43,8 +43,9 @@ module Hue
def initialize(name)
@name = name
- @state = self.class.alias_map[name.to_sym]
- raise Error, "Unknown light state: #{name}" if @state.nil?
+ @state = self.class.alias_map.fetch(name.to_sym) do
+ raise Error, "Unknown light state: #{name}"
+ end
end
end
end
diff --git a/hue/lib/hue/colors.rb b/hue/lib/hue/colors.rb
@@ -13,13 +13,8 @@ module Hue
when 1
Colors::ColorTemperature.new(args.first)
when 2
- a = args.first.to_f
- _ = args.last.to_f
- if a > 1.0
- Colors::HueSaturation.new(args.first, args.last)
- else
- Colors::XY.new(*args)
- end
+ first, last = args
+ first.to_f > 1.0 ? Colors::HueSaturation.new(first, last) : Colors::XY.new(first, last)
when 3
Colors::RGB.new(*args)
else
diff --git a/hue/lib/hue/colors/color.rb b/hue/lib/hue/colors/color.rb
@@ -6,7 +6,7 @@ module Hue
ERROR_METHOD_NOT_IMPLEMENTED = 'method-not-implemented'
def self.ranged(min, val, max)
- [[min, val].max, max].min
+ val.clamp(min, max)
end
def to_hash
diff --git a/hue/lib/hue/colors/color_temperature.rb b/hue/lib/hue/colors/color_temperature.rb
@@ -52,21 +52,17 @@ module Hue
# using method described at
# http://www.tannerhelland.com/4435/convert-temperature-rgb-algorithm-code/
temp = kelvin / 100
+ cool = temp <= 66
+ offset = temp - 60
- red = temp <= 66 ? 255 : 329.698727446 * ((temp - 60)**-0.1332047592)
-
- green = if temp <= 66
- 99.4708025861 * Math.log(temp) - 161.1195681661
- else
- 288.1221695283 * ((temp - 60)**-0.0755148492)
- end
-
+ red = cool ? 255 : 329.698727446 * (offset**-0.1332047592)
+ green = cool ? (99.4708025861 * Math.log(temp)) - 161.1195681661 : 288.1221695283 * (offset**-0.0755148492)
blue = if temp >= 66
255
elsif temp <= 19
0
else
- 138.5177312231 * Math.log(temp - 10) - 305.0447927307
+ (138.5177312231 * Math.log(temp - 10)) - 305.0447927307
end
RGB.new(red, green, blue)
@@ -75,7 +71,7 @@ module Hue
private
def unit_to_mired_interval(unit_interval)
- unit_interval * (MIRED_MAX - MIRED_MIN) + MIRED_MIN
+ (unit_interval * (MIRED_MAX - MIRED_MIN)) + MIRED_MIN
end
end
end
diff --git a/hue/lib/hue/colors/hue_saturation.rb b/hue/lib/hue/colors/hue_saturation.rb
@@ -66,11 +66,12 @@ module Hue
else
v = 1.0 # We are setting the value to 1. Don't count brightness here
- i = (h * 6).floor
- f = h * 6 - i
+ h6 = h * 6
+ i = h6.floor
+ f = h6 - i
p = v * (1 - s)
- q = v * (1 - f * s)
- t = v * (1 - (1 - f) * s)
+ q = v * (1 - (f * s))
+ t = v * (1 - ((1 - f) * s))
case i % 6
when 0
@@ -106,11 +107,11 @@ module Hue
private
def unit_to_hue_interval(value)
- (value * (HUE_MAX - HUE_MIN) + HUE_MIN).round
+ ((value * (HUE_MAX - HUE_MIN)) + HUE_MIN).round
end
def unit_to_saturation_interval(value)
- (value * (SATURATION_MAX - SATURATION_MIN) + SATURATION_MIN).round
+ ((value * (SATURATION_MAX - SATURATION_MIN)) + SATURATION_MIN).round
end
end
end
diff --git a/hue/lib/hue/colors/rgb.rb b/hue/lib/hue/colors/rgb.rb
@@ -33,16 +33,13 @@ module Hue
end
def to_hash
- max = MAX.to_f
- red = self.red / max
- green = self.green / max
- blue = self.red / max
+ scale = MAX.to_f
+ red = self.red / scale
+ green = self.green / scale
+ blue = self.blue / scale
max = [red, green, blue].max
min = [red, green, blue].min
- _ = 0
- _ = ((max + min) / 2 * 255)
-
d = max - min
s = max.zero? ? 0 : (d / max * 255)
@@ -50,11 +47,11 @@ module Hue
when min
0 # monochromatic
when red
- (green - blue) / d + (green < blue ? 6 : 0)
+ ((green - blue) / d) + (green < blue ? 6 : 0)
when green
- (blue - red) / d + 2
+ ((blue - red) / d) + 2
when blue
- (red - green) / d + 4
+ ((red - green) / d) + 4
end * 60
h = (h * HueSaturation::HUE_SCALE).to_i
@@ -87,7 +84,7 @@ module Hue
end
def unit_to_rgb_scale(value)
- (value * (MAX - MIN) + MIN).round
+ ((value * (MAX - MIN)) + MIN).round
end
def parse(value)
diff --git a/hue/lib/hue/colors/xy.rb b/hue/lib/hue/colors/xy.rb
@@ -14,10 +14,10 @@ module Hue
attr_reader :x, :y
- def initialize(*xy)
+ def initialize(*coords)
super()
- self.x = xy.first
- self.y = xy.last
+ self.x = coords.first
+ self.y = coords.last
end
def x=(value)
diff --git a/hue/lib/hue/config/abstract.rb b/hue/lib/hue/config/abstract.rb
@@ -25,7 +25,8 @@ module Hue
yaml ||= {}
if yaml.key?(name) && !overwrite_existing_key
- raise "Key named '#{name}' already exists in config file '#{path}'.\nPlease remove it before creating a new one with the same name."
+ raise "Key named '#{name}' already exists in config file '#{path}'.\n" \
+ 'Please remove it before creating a new one with the same name.'
end
add_self_to_yaml(yaml)
@@ -36,7 +37,7 @@ module Hue
yaml = begin
YAML.load_file(path)
rescue StandardError
- Hash::New
+ {}
end
yaml.delete(name) if yaml.key?(name)
@@ -65,7 +66,7 @@ module Hue
class << self
def setup_file_path(path)
dir = File.dirname(path)
- FileUtils.mkdir_p(dir) unless Dir.exist?(dir)
+ FileUtils.mkdir_p(dir)
end
def read_file(config_file)
diff --git a/hue/lib/hue/config/application.rb b/hue/lib/hue/config/application.rb
@@ -8,7 +8,7 @@ module Hue
STRING_ID = 'id'
def self.hue_config_dir
- File.join(ENV['HOME'], '.hue')
+ File.join(Dir.home, '.hue')
end
def self.file_path
@@ -42,7 +42,7 @@ module Hue
end
def ==(other)
- super(other) &&
+ super &&
bridge_id == other.bridge_id &&
id == other.id
end
@@ -53,7 +53,7 @@ module Hue
key = name.dup.force_encoding('ASCII') # Avoid binary encoded YAML
bridge = bridge_id.dup.force_encoding('ASCII')
yaml[key] = {
- STRING_ID => id.force_encoding('ASCII'),
+ STRING_ID => id.dup.force_encoding('ASCII'),
STRING_BRIDGE_ID => bridge
}
end
diff --git a/hue/lib/hue/config/bridge.rb b/hue/lib/hue/config/bridge.rb
@@ -4,7 +4,7 @@ module Hue
module Config
class Bridge < Abstract
def self.hue_config_dir
- File.join(ENV['HOME'], '.hue')
+ File.join(Dir.home, '.hue')
end
def self.file_path
@@ -33,7 +33,7 @@ module Hue
end
def write(overwrite_existing_key: true)
- super(overwrite_existing_key: overwrite_existing_key)
+ super
end
private
diff --git a/hue/lib/hue/utilities/indentation.rb b/hue/lib/hue/utilities/indentation.rb
@@ -15,14 +15,16 @@ module Indentation
reset
+ # :reek:UtilityFunction { exclude: [ "Indentation#indent" ] }
def indent(size = DEFAULT_INDENTATION_SIZE)
original_size = Indentation.indentation_size
+ original_level = Indentation.indentation_level
Indentation.indentation_size = size
- Indentation.indentation_level += 1
+ Indentation.indentation_level = original_level + 1
yield
ensure
Indentation.indentation_size = original_size
- Indentation.indentation_level -= 1
+ Indentation.indentation_level = original_level
end
def puts(*message)
diff --git a/hue/pkg/hue-0.1.0.gem b/hue/pkg/hue-0.1.0.gem
Binary files differ.
diff --git a/hue/sig/.keep b/hue/sig/.keep
diff --git a/hue/sorbet/config b/hue/sorbet/config
@@ -0,0 +1,7 @@
+--dir
+.
+--ignore=/pkg/
+--ignore=/vendor/
+--ignore=/coverage/
+--ignore=/tmp/
+--ignore=/spec/
diff --git a/hue/spec/cli_spec.rb b/hue/spec/cli_spec.rb
@@ -4,12 +4,12 @@ require 'spec_helper'
describe Hue::CLI do
it 'can be loaded' do
- expect(Hue::CLI).to be_a(Module)
+ expect(described_class).to be_a(Module)
end
it 'has valid commands' do
# Trigger object space loading
- Hue::CLI.send(:commands_in_object_space)
+ described_class.send(:commands_in_object_space)
# Just ensure it doesn't crash
end
end
diff --git a/hue/spec/hue/bridge_spec.rb b/hue/spec/hue/bridge_spec.rb
@@ -1,57 +1,56 @@
-require 'spec_helper.rb'
+# frozen_string_literal: true
-describe Hue::Bridge do
+require 'spec_helper'
+describe Hue::Bridge do
context 'when instantiated' do
let(:bridge) { test_bridge }
- it 'should report the status' do
+ it 'reports the status' do
with_fake_request
expect(bridge.status).to eq(api_reply(:get_success))
end
- it 'should report errors' do
+ it 'reports errors' do
with_fake_request(:lights, :unauthorized)
- expect {
+ expect do
bridge.lights
- }.to raise_error(Hue::API::Error, 'unauthorized user')
+ end.to raise_error(Hue::API::Error, 'unauthorized user')
end
- it 'should report the available lights' do
+ it 'reports the available lights' do
with_fake_request(:lights)
expect(bridge.lights).to eq(api_reply(:lights))
end
- it 'should report a simple string of light names' do
+ it 'reports a simple string of light names' do
with_fake_request(:lights)
expect(bridge.light_names).to eq("1. Dining\n2. Bedroom Far\n3. Bedroom Near")
end
- it 'should report the bridge config' do
+ it 'reports the bridge config' do
with_fake_request(:config)
expect(bridge.config).to eq(api_reply(:config))
end
- it 'should report the light schedules' do
+ it 'reports the light schedules' do
with_fake_request(:schedules)
expect(bridge.schedules).to eq(api_reply(:schedules))
end
- it 'should return instance of all the bulbs' do
+ it 'returns instance of all the bulbs' do
with_fake_request(:lights)
bulbs = bridge.bulbs
expect(bulbs.size).to eq(3)
- bulbs.each do |bulb|
- expect(bulb).to be_a(Hue::Bulb)
- end
+ expect(bulbs).to all(be_a(Hue::Bulb))
end
- it 'should allow unregistering an existing config' do
+ it 'allows unregistering an existing config' do
with_fake_delete("config/whitelist/#{TEST_APPLICATION_UUID}")
bridge.unregister
end
- it 'should allow scanning for new lights' do
+ it 'allows scanning for new lights' do
with_fake_post("#{TEST_APPLICATION_UUID}/lights")
bridge.add_lights
end
@@ -61,11 +60,10 @@ describe Hue::Bridge do
let(:new_id) { 'new_test_id' }
let(:bridge) { described_class.new(new_id, TEST_BRIDGE_URI) }
- it 'should allow registering the new config' do
+ it 'allows registering the new config' do
mock_socket_hostname
- with_fake_post(nil, {:devicetype => "hue-lib@hostname"})
+ with_fake_post(nil, { devicetype: "#{Hue::DEVICE_TYPE}@hostname" })
bridge.register
end
end
-
end
diff --git a/hue/spec/hue/bulb_spec.rb b/hue/spec/hue/bulb_spec.rb
@@ -1,72 +1,72 @@
-require 'spec_helper.rb'
+# frozen_string_literal: true
-describe Hue::Bulb do
+require 'spec_helper'
+describe Hue::Bulb do
context 'when instantiated with a given bridge and id' do
let(:bulb) { described_class.new(test_bridge, 1) }
- before(:each) do
+ before do
with_fake_request('lights/1')
@test_status = api_reply('lights/1')
bulb.refresh!
end
- it 'should report the bulb state' do
+ it 'reports the bulb state' do
expect(bulb.state).to eq(@test_status['state'])
end
- it 'should report the bulb info' do
+ it 'reports the bulb info' do
info = api_reply('lights/1')
info.delete('state')
info.delete('pointsymbol')
expect(bulb.info).to eq(info)
end
- it "should report it's name" do
+ it "reports it's name" do
expect(bulb.name).to eq(@test_status['name'])
end
- it "should report if it's on" do
+ it "reports if it's on" do
expect(bulb.on?).to be false
end
- it "should report if it's off" do
+ it "reports if it's off" do
expect(bulb.off?).to be true
end
- it "should report the brightness and color mode" do
+ it 'reports the brightness and color mode' do
expect(bulb.brightness).to eq(146)
expect(bulb.bri).to eq(146)
expect(bulb.color_mode).to eq('ct')
expect(bulb.color_mode).to eq(bulb.colormode)
end
- it "should report the color" do
+ it 'reports the color' do
color = bulb.color
expect(color).to be_a(Hue::Colors::ColorTemperature)
expect(color.mired).to eq(459)
expect(color.kelvin).to eq(2179)
end
- it "should report the alert state" do
+ it 'reports the alert state' do
expect(bulb.alert).to eq('none')
expect(bulb.blinking?).to be false
expect(bulb.solid?).to be true
end
- it "should report the effect state" do
+ it 'reports the effect state' do
expect(bulb.effect?).to be false
expect(bulb.effect).to eq('none')
expect(bulb.color_loop?).to be false
end
- it "should report the transition time" do
+ it 'reports the transition time' do
expect(bulb.transition_time).to eq(0)
end
context 'by changing state' do
-
- it 'should allow turning bulps on and off' do
+ it 'allows turning bulps on and off' do
with_fake_update('lights/1/state', on: true)
expect(bulb.on).to be true
@@ -74,38 +74,38 @@ describe Hue::Bulb do
expect(bulb.off).to be true
end
- it 'should allow changing the name' do
+ it 'allows changing the name' do
with_fake_update('lights/1', name: 'New name')
bulb.name = 'New name'
expect(bulb.name).to eq('New name')
end
- it 'should allow setting hue, saturation and brightness' do
- color = Hue::Colors::HueSaturation.new(21845, 1293)
+ it 'allows setting hue, saturation and brightness' do
+ color = Hue::Colors::HueSaturation.new(21_845, 1293)
- with_fake_update('lights/1/state', hue: 21845, sat: 255)
+ with_fake_update('lights/1/state', hue: 21_845, sat: 255)
set_color = (bulb.color = color)
- expect(set_color.hue).to eq(21845)
+ expect(set_color.hue).to eq(21_845)
expect(set_color.saturation).to eq(255)
end
- it 'should allow setting brightness as a number, percentage or string' do
+ it 'allows setting brightness as a number, percentage or string' do
with_fake_update('lights/1/state', bri: 233)
bulb.brightness = 233
expect(bulb.brightness).to eq(233)
with_fake_update('lights/1/state', bri: 128)
- bulb.brightness = "50%"
+ bulb.brightness = '50%'
expect(bulb.brightness).to eq(128)
expect(bulb.brightness_in_unit_interval).to eq(0.5019607843137255)
expect(bulb.brightness_percent).to eq(50)
with_fake_update('lights/1/state', bri: 128)
- bulb.brightness = "128"
+ bulb.brightness = '128'
expect(bulb.brightness).to eq(128)
end
- it 'should allow setting blink, solid and flash alerts' do
+ it 'allows setting blink, solid and flash alerts' do
with_fake_update('lights/1/state', alert: 'lselect')
bulb.blink
expect(bulb.blinking?).to be true
@@ -125,7 +125,7 @@ describe Hue::Bulb do
end
end
- it 'should allow setting colorloop, and effect' do
+ it 'allows setting colorloop, and effect' do
with_fake_update('lights/1/state', effect: 'new')
bulb.effect = 'new'
expect(bulb.effect).to eq('new')
@@ -146,15 +146,12 @@ describe Hue::Bulb do
expect(bulb.effect).to eq('colorloop')
end
- it 'should allow setting the transitions time, and employ it for a state change' do
+ it 'allows setting the transitions time, and employ it for a state change' do
bulb.transition_time = 10
with_fake_update('lights/1/state', transitiontime: 100, bri: 255)
bulb.brightness = 255
end
-
end
-
end
-
end
diff --git a/hue/spec/hue/cli/command_spec.rb b/hue/spec/hue/cli/command_spec.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Hue::CLI::Command do
+ before do
+ mock_application_config_path
+ mock_bridge_config_path
+ allow(Hue::CLI).to receive(:bridge).and_return(test_bridge)
+ end
+
+ let(:subclass) do
+ Class.new(described_class) do
+ def self.name
+ 'Hue::CLI::Commands::Test'
+ end
+
+ def hello(*_args)
+ :hi
+ end
+ end
+ end
+
+ it 'exposes the configured bridge' do
+ expect(described_class.new.bridge).to be_a(Hue::Bridge)
+ end
+
+ it 'dispatches to a defined method' do
+ expect(subclass.new.send(:send_method, 'hello')).to eq(:hi)
+ end
+
+ it 'raises Hue::CLI::Error for an unavailable method' do
+ expect { subclass.new.send(:send_method, 'nope') }
+ .to raise_error(Hue::CLI::Error, /not available/)
+ end
+
+ it 'falls back to bridge.print_state when execute called with no args' do
+ instance = described_class.new
+ expect(instance.bridge).to receive(:print_state)
+ instance.execute
+ end
+end
diff --git a/hue/spec/hue/cli/commands/delete_spec.rb b/hue/spec/hue/cli/commands/delete_spec.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Hue::CLI::Commands::Delete do
+ before do
+ mock_application_config_path
+ mock_bridge_config_path
+ allow(Hue::CLI).to receive(:bridge).and_return(test_bridge)
+ end
+
+ let(:command) { described_class.new }
+
+ it 'unregisters the current bridge when called with no args' do
+ with_fake_delete("config/whitelist/#{TEST_APPLICATION_UUID}")
+ command.execute
+ end
+
+ it 'unregisters a specific application id when given' do
+ other_id = 'other_app'
+ stub_request(:delete, "#{TEST_BRIDGE_URI}/#{other_id}/config/whitelist/#{other_id}")
+ .to_return(body: '[{}]', status: 200, headers: { 'Content-Type' => 'application/json' })
+ command.execute(other_id)
+ end
+end
diff --git a/hue/spec/hue/cli/commands/light_spec.rb b/hue/spec/hue/cli/commands/light_spec.rb
@@ -0,0 +1,65 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Hue::CLI::Commands::Light do
+ before do
+ mock_application_config_path
+ mock_bridge_config_path
+ allow(Hue::CLI).to receive(:bridge).and_return(test_bridge)
+ with_fake_request('lights/1')
+ end
+
+ let(:command) { described_class.new }
+
+ it 'rejects non-numeric ids' do
+ expect { command.execute('xyz') }
+ .to raise_error(Hue::CLI::Error, /Not a valid light number/)
+ end
+
+ it 'prints state when only an id is given' do
+ out = with_stdout { command.execute('1') }
+ expect(out).to include('Living')
+ end
+
+ it 'turns the bulb on' do
+ with_fake_update('lights/1/state', on: true)
+ command.execute('1', 'on')
+ end
+
+ it 'turns the bulb off' do
+ with_fake_update('lights/1/state', on: false)
+ command.execute('1', 'off')
+ end
+
+ it 'sets brightness' do
+ with_fake_update('lights/1/state', bri: 200)
+ command.execute('1', 'brightness', '200')
+ end
+
+ it 'sets color' do
+ with_fake_update('lights/1/state', hue: 30_000, sat: 200)
+ command.execute('1', 'color', '30000', '200')
+ end
+
+ it 'sets the name' do
+ with_fake_update('lights/1', name: 'Kitchen')
+ command.execute('1', 'name', 'Kitchen')
+ end
+
+ it 'starts a colorloop effect' do
+ with_fake_update('lights/1/state', effect: 'colorloop')
+ command.execute('1', 'colorloop')
+ end
+
+ it 'clears effect and alert with bare clear' do
+ with_fake_update('lights/1/state', effect: 'none')
+ with_fake_update('lights/1/state', alert: 'none')
+ command.execute('1', 'clear')
+ end
+
+ it 'dumps state as JSON' do
+ out = with_stdout { command.execute('1', 'dump') }
+ expect(JSON.parse(out)).to be_a(Hash)
+ end
+end
diff --git a/hue/spec/hue/cli/commands/lights_spec.rb b/hue/spec/hue/cli/commands/lights_spec.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Hue::CLI::Commands::Lights do
+ before do
+ mock_application_config_path
+ mock_bridge_config_path
+ allow(Hue::CLI).to receive(:bridge).and_return(test_bridge)
+ end
+
+ let(:command) { described_class.new }
+
+ it 'prints all bulbs when called with no args' do
+ expect(command.bridge).to receive(:print_bulbs)
+ command.execute
+ end
+
+ it 'invokes find when "find" is passed' do
+ expect(command.bridge).to receive(:add_lights)
+ command.execute('find')
+ end
+
+ it 'applies an unknown action to every light' do
+ with_fake_request(:lights)
+ expect(Hue::CLI::Commands::Light).to receive(:new).exactly(3).times.and_return(
+ instance_double(Hue::CLI::Commands::Light, execute: nil)
+ )
+ command.execute('on')
+ end
+end
diff --git a/hue/spec/hue/cli_spec.rb b/hue/spec/hue/cli_spec.rb
@@ -0,0 +1,59 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Hue::CLI do
+ before do
+ mock_application_config_path
+ mock_bridge_config_path
+ allow(Hue.logger).to receive(:warn)
+ described_class.retry_count = 0
+ end
+
+ describe '.run' do
+ it 'prints default state when called with no args' do
+ with_fake_request
+ with_fake_request(:lights)
+ out = with_stdout { described_class.run([]) }
+ expect(out).to include('Philips hue')
+ end
+
+ it 'dispatches a known command by name' do
+ with_fake_request(:lights)
+ stub_request(:get, %r{#{TEST_BRIDGE_URI}/#{TEST_APPLICATION_UUID}/lights/\d+})
+ .to_return(body: File.read(File.join(TestHelpers::JSON_FIXTURE_DIR, 'lights/1.json')),
+ status: 200, headers: { 'Content-Type' => 'application/json' })
+ out = with_stdout { described_class.run(%w[lights]) }
+ expect(out).to include('Living')
+ end
+
+ it 'dispatches a numeric arg to the Light command' do
+ with_fake_request('lights/1')
+ out = with_stdout { described_class.run(%w[1]) }
+ expect(out).to include('Living')
+ end
+
+ it 'reports unknown commands' do
+ out = with_stdout { described_class.run(%w[bogus]) }
+ expect(out).to include('Unknown command: bogus')
+ end
+
+ it 'shows registration instructions when not registered' do
+ stub_request(:any, /.*/).to_return(status: 200, body: '{}')
+ allow(Hue::Config::Application).to receive(:default)
+ .and_raise(Hue::Config::NotFound.new('not found'))
+ mock_udp_replies
+ out = with_stdout { described_class.run([]) }
+ expect(out).to include('Application not registered.')
+ end
+
+ it 'gives up after several retries on bridge connection problems' do
+ fake_bridge = instance_double(Hue::Bridge, print_config: nil)
+ allow(described_class).to receive(:bridge).and_return(fake_bridge)
+ allow(fake_bridge).to receive(:print_state).and_raise(Hue::Error, 'execution expired')
+ allow(Hue).to receive(:register_bridges).and_return({})
+ out = with_stdout { described_class.run([]) }
+ expect(out).to include('Giving up contacting bridge')
+ end
+ end
+end
diff --git a/hue/spec/hue/colors/color_spec.rb b/hue/spec/hue/colors/color_spec.rb
@@ -1,17 +1,18 @@
-require 'spec_helper.rb'
+# frozen_string_literal: true
-describe Hue::Colors::Color do
+require 'spec_helper'
+describe Hue::Colors::Color do
context 'implements a ranged method: max > value > min' do
- it 'should return the min when value < min' do
+ it 'returns the min when value < min' do
expect(described_class.ranged(0, -1, 2)).to eq(0)
end
- it 'should return the max when value > max' do
+ it 'returns the max when value > max' do
expect(described_class.ranged(0, 3, 2)).to eq(2)
end
- it 'should return the value when max > value > min' do
+ it 'returns the value when max > value > min' do
expect(described_class.ranged(0, 1, 2)).to eq(1)
end
end
@@ -19,9 +20,9 @@ describe Hue::Colors::Color do
def abstract_method(method)
abstract_color = described_class.new
expect(abstract_color).to respond_to(method)
- expect {
+ expect do
abstract_color.send(method)
- }.to raise_error(described_class::ERROR_METHOD_NOT_IMPLEMENTED)
+ end.to raise_error(described_class::ERROR_METHOD_NOT_IMPLEMENTED)
end
context 'defines but does not implement methods:' do
@@ -29,5 +30,4 @@ describe Hue::Colors::Color do
it('#to_s') { abstract_method(:to_s) }
it('#to_rgb') { abstract_method(:to_rgb) }
end
-
end
diff --git a/hue/spec/hue/colors/color_temperature_spec.rb b/hue/spec/hue/colors/color_temperature_spec.rb
@@ -1,70 +1,69 @@
-# Encoding: UTF-8
-require 'spec_helper.rb'
+# frozen_string_literal: true
-describe Hue::Colors::ColorTemperature do
+require 'spec_helper'
+describe Hue::Colors::ColorTemperature do
context 'when initialized with a valid value' do
let(:color) { described_class.new(500) }
- it 'should report the temperature in mireds and kelvins' do
+ it 'reports the temperature in mireds and kelvins' do
expect(color.mired).to eq(500)
expect(color.kelvin).to eq(2000)
end
- it 'should have a string representation' do
- expect(color.to_s).to eq("Temperature=2000°K (500 mired)")
+ it 'has a string representation' do
+ expect(color.to_s).to eq('Temperature=2000°K (500 mired)')
end
- it 'should have a hash representation' do
- expect(color.to_hash).to eq({ct: 500})
+ it 'has a hash representation' do
+ expect(color.to_hash).to eq({ ct: 500 })
end
- it 'should have an RGB representation' do
- expect(color.to_rgb).to eq(Hue::Colors::RGB.new(255,136,13))
+ it 'has an RGB representation' do
+ expect(color.to_rgb).to eq(Hue::Colors::RGB.new(255, 136, 13))
end
context 'when allowing change to the temperature value' do
- it 'should go to the max in kelvins' do
+ it 'goes to the max in kelvins' do
color.kelvin = 7000
expect(color.kelvin).to eq(described_class::KELVIN_MAX)
expect(color.mired).to eq(described_class::MIRED_MIN)
end
- it 'should go to the max in mireds' do
+ it 'goes to the max in mireds' do
color.mired = 600
expect(color.mired).to eq(described_class::MIRED_MAX)
expect(color.kelvin).to eq(described_class::KELVIN_MIN)
end
- it 'should go to the min in mireds' do
+ it 'goes to the min in mireds' do
color.mired = 100
expect(color.mired).to eq(described_class::MIRED_MIN)
expect(color.kelvin).to eq(described_class::KELVIN_MAX)
end
- it 'should go to the min in kelvins' do
+ it 'goes to the min in kelvins' do
color.mired = 2000
expect(color.mired).to eq(described_class::MIRED_MAX)
expect(color.kelvin).to eq(described_class::KELVIN_MIN)
end
- it 'should hit the middle' do
- mired_middle = described_class::MIRED_MAX/2
+ it 'hits the middle' do
+ mired_middle = described_class::MIRED_MAX / 2
color.mired = mired_middle
expect(color.mired).to eq(mired_middle)
expect(color.kelvin).to eq(4000)
- kelvin_middle = described_class::KELVIN_MAX/2
+ kelvin_middle = described_class::KELVIN_MAX / 2
color.kelvin = kelvin_middle
expect(color.kelvin).to eq(kelvin_middle)
expect(color.mired).to eq(307)
end
-
end
end
context 'when initializing' do
- it 'should set the temperature depending on the value' do
+ it 'sets the temperature depending on the value' do
color = described_class.new(100)
expect(color.mired).to eq(described_class::MIRED_MIN)
@@ -78,29 +77,28 @@ describe Hue::Colors::ColorTemperature do
expect(color.kelvin).to eq(described_class::KELVIN_MAX)
end
- it 'should report kelvins as an integer' do
+ it 'reports kelvins as an integer' do
color = described_class.new(459)
expect(color.kelvin).to eq(2179)
end
- it 'should accept a string value for the temperature' do
- color = described_class.new("100")
+ it 'accepts a string value for the temperature' do
+ color = described_class.new('100')
expect(color.mired).to eq(described_class::MIRED_MIN)
color = described_class.new((described_class::KELVIN_MAX + 3000).to_s)
expect(color.kelvin).to eq(described_class::KELVIN_MAX)
end
- it 'should allow a percentage value for the mired scale' do
- color = described_class.new("0%")
+ it 'allows a percentage value for the mired scale' do
+ color = described_class.new('0%')
expect(color.mired).to eq(described_class::MIRED_MIN)
- color = described_class.new("50%")
+ color = described_class.new('50%')
expect(color.mired).to eq(326)
- color = described_class.new("100%")
+ color = described_class.new('100%')
expect(color.mired).to eq(described_class::MIRED_MAX)
end
end
-
end
diff --git a/hue/spec/hue/colors/hue_saturation_spec.rb b/hue/spec/hue/colors/hue_saturation_spec.rb
@@ -1,59 +1,59 @@
-# Encoding: UTF-8
-require 'spec_helper.rb'
+# frozen_string_literal: true
-describe Hue::Colors::HueSaturation do
+require 'spec_helper'
+describe Hue::Colors::HueSaturation do
context 'when initialized with valid values' do
let(:color) { described_class.new(2e4, 122) }
- it 'should report those values' do
+ it 'reports those values' do
expect(color.hue).to eq(20_000)
expect(color.saturation).to eq(122)
end
- it 'should report hue in degrees and a closed unit interval' do
+ it 'reports hue in degrees and a closed unit interval' do
expect(color.hue_in_degrees).to eq(109.86328125)
expect(color.hue_in_unit_interval).to eq(0.30517578125)
end
- it 'should report saturation in a closed unit interval' do
+ it 'reports saturation in a closed unit interval' do
expect(color.saturation_in_unit_interval).to eq(0.47843137254901963)
end
- it 'should have a string representation' do
- expect(color.to_s).to eq("Hue=20000, Saturation=122")
+ it 'has a string representation' do
+ expect(color.to_s).to eq('Hue=20000, Saturation=122')
end
- it 'should have a hash representation' do
- expect(color.to_hash).to eq({hue: 20_000, sat: 122})
+ it 'has a hash representation' do
+ expect(color.to_hash).to eq({ hue: 20_000, sat: 122 })
end
- it 'should have an RGB representation' do
- expect(color.to_rgb).to eq(Hue::Colors::RGB.new(153,255,132))
+ it 'has an RGB representation' do
+ expect(color.to_rgb).to eq(Hue::Colors::RGB.new(153, 255, 132))
end
context 'when allowing change to the color values' do
- it 'should allow setting the maximum hue' do
+ it 'allows setting the maximum hue' do
color.hue = 70_000
expect(color.hue).to eq(described_class::HUE_MAX)
expect(color.hue_in_degrees).to eq(360)
expect(color.hue_in_unit_interval).to eq(1.0)
end
- it 'should allow setting the minimum hue' do
+ it 'allows setting the minimum hue' do
color.hue = -1000
expect(color.hue).to eq(described_class::HUE_MIN)
expect(color.hue_in_degrees).to eq(0)
expect(color.hue_in_unit_interval).to eq(0.0)
end
- it 'should allow setting the maximum saturation' do
+ it 'allows setting the maximum saturation' do
color.saturation = 300
expect(color.sat).to eq(described_class::SATURATION_MAX)
expect(color.sat_in_unit_interval).to eq(1.0)
end
- it 'should allow setting the minimum saturation' do
+ it 'allows setting the minimum saturation' do
color.saturation = -1
expect(color.sat).to eq(described_class::SATURATION_MIN)
expect(color.sat_in_unit_interval).to eq(0.0)
@@ -61,31 +61,29 @@ describe Hue::Colors::HueSaturation do
end
context 'when initialized with other values' do
- it 'should accept hue and saturation values' do
+ it 'accepts hue and saturation values' do
color = described_class.new(3e4, 200)
expect(color.hue).to eq(3e4)
expect(color.sat).to eq(200)
end
- it 'should accept a percentage of the hue scale with saturation value' do
+ it 'accepts a percentage of the hue scale with saturation value' do
color = described_class.new('20%', 150)
- expect(color.hue).to eq(13107)
+ expect(color.hue).to eq(13_107)
expect(color.sat).to eq(150)
end
- it 'should accept a hue value with a percentage of the saturation scale' do
+ it 'accepts a hue value with a percentage of the saturation scale' do
color = described_class.new(7e4, '100%')
expect(color.hue).to eq(Hue::Colors::HueSaturation::HUE_MAX)
expect(color.sat).to eq(Hue::Colors::HueSaturation::SATURATION_MAX)
end
- it 'should accept hue and saturation strings' do
- color = described_class.new("30_000", "1333")
+ it 'accepts hue and saturation strings' do
+ color = described_class.new('30_000', '1333')
expect(color.hue).to eq(3e4)
expect(color.sat).to eq(Hue::Colors::HueSaturation::SATURATION_MAX)
end
end
-
end
-
end
diff --git a/hue/spec/hue/colors/rgb_spec.rb b/hue/spec/hue/colors/rgb_spec.rb
@@ -1,31 +1,31 @@
-# Encoding: UTF-8
-require 'spec_helper.rb'
+# frozen_string_literal: true
-describe Hue::Colors::RGB do
+require 'spec_helper'
+describe Hue::Colors::RGB do
context 'when initialized with a valid values' do
let(:color) { described_class.new(64, 128, 248) }
- it 'should report those values' do
+ it 'reports those values' do
expect(color.red).to eq(64)
expect(color.green).to eq(128)
expect(color.blue).to eq(248)
end
- it 'should have a string representation' do
- expect(color.to_s).to eq("RGB≈[64, 128, 248]")
+ it 'has a string representation' do
+ expect(color.to_s).to eq('RGB≈[64, 128, 248]')
end
- it 'should have a hash representation' do
- expect(color.to_hash).to eq({hue: 21845, sat: 127, bri: 255 })
+ it 'has a hash representation' do
+ expect(color.to_hash).to eq({ hue: 39_891, sat: 189, bri: 255 })
end
- it 'should have an RGB representation' do
+ it 'has an RGB representation' do
expect(color.to_rgb).to eq(color)
end
context 'when allowing change to the color values' do
- it 'should allow setting the maximum' do
+ it 'allows setting the maximum' do
color.red = 300
expect(color.red).to eq(described_class::MAX)
color.green = 255
@@ -34,7 +34,7 @@ describe Hue::Colors::RGB do
expect(color.red).to eq(described_class::MAX)
end
- it 'should allow setting the minimum' do
+ it 'allows setting the minimum' do
color.red = 0
expect(color.red).to eq(described_class::MIN)
color.green = -1
@@ -45,14 +45,14 @@ describe Hue::Colors::RGB do
end
context 'when initializing' do
- it 'should allow strings' do
+ it 'allows strings' do
color = described_class.new('200', '100', '50')
expect(color.red).to eq(200)
expect(color.green).to eq(100)
expect(color.blue).to eq(50)
end
- it 'should allow percentages' do
+ it 'allows percentages' do
color = described_class.new('10%', '100%', '50%')
expect(color.red).to eq(26)
expect(color.green).to eq(255)
@@ -60,5 +60,4 @@ describe Hue::Colors::RGB do
end
end
end
-
end
diff --git a/hue/spec/hue/colors/xy_spec.rb b/hue/spec/hue/colors/xy_spec.rb
@@ -1,37 +1,37 @@
-# Encoding: UTF-8
-require 'spec_helper.rb'
+# frozen_string_literal: true
-describe Hue::Colors::XY do
+require 'spec_helper'
+describe Hue::Colors::XY do
context 'when initialized with a valid values' do
let(:color) { described_class.new(0.5, 0.5) }
- it 'should report those values' do
+ it 'reports those values' do
expect(color.x).to eq(0.5)
expect(color.y).to eq(0.5)
end
- it 'should have a string representation' do
- expect(color.to_s).to eq("XY=[0.5, 0.5]")
+ it 'has a string representation' do
+ expect(color.to_s).to eq('XY=[0.5, 0.5]')
end
- it 'should have a hash representation' do
- expect(color.to_hash).to eq({xy: [0.5, 0.5]})
+ it 'has a hash representation' do
+ expect(color.to_hash).to eq({ xy: [0.5, 0.5] })
end
- it 'should have an RGB representation' do
- expect(color.to_rgb).to eq(Hue::Colors::RGB.new(217,209,41))
+ it 'has an RGB representation' do
+ expect(color.to_rgb).to eq(Hue::Colors::RGB.new(217, 209, 41))
end
context 'when allowing change to the color values' do
- it 'should allow setting the maximum' do
+ it 'allows setting the maximum' do
color.x = 1.1
expect(color.x).to eq(described_class::MAX)
color.y = 2
expect(color.y).to eq(described_class::MAX)
end
- it 'should allow setting the minimum' do
+ it 'allows setting the minimum' do
color.x = -1
expect(color.x).to eq(described_class::MIN)
color.y = 0
@@ -40,12 +40,11 @@ describe Hue::Colors::XY do
end
context 'when initializing' do
- it 'should allow strings' do
+ it 'allows strings' do
color = described_class.new('0.22', '10.00')
expect(color.x).to eq(0.22)
expect(color.y).to eq(1.00)
end
end
end
-
end
diff --git a/hue/spec/hue/colors_spec.rb b/hue/spec/hue/colors_spec.rb
@@ -1,38 +1,39 @@
-require 'spec_helper.rb'
+# frozen_string_literal: true
-describe Hue::Colors do
+require 'spec_helper'
+describe Hue::Colors do
context 'when #parse_state is used' do
it 'can return a ColorTemperature' do
- color = described_class.parse_state({'colormode' => 'ct', 'ct' => 500})
+ color = described_class.parse_state({ 'colormode' => 'ct', 'ct' => 500 })
expect(color).to be_a(Hue::Colors::ColorTemperature)
expect(color.mired).to eq(500)
end
it 'can return HueSaturation' do
- color = described_class.parse_state({'colormode' => 'hs', 'hue' => 30_000, 'sat' => 150})
+ color = described_class.parse_state({ 'colormode' => 'hs', 'hue' => 30_000, 'sat' => 150 })
expect(color).to be_a(Hue::Colors::HueSaturation)
expect(color.hue).to eq(30_000)
expect(color.sat).to eq(150)
end
it 'can return XY' do
- color = described_class.parse_state({'colormode' => 'xy', 'xy' => [0.8, 0.2]})
+ color = described_class.parse_state({ 'colormode' => 'xy', 'xy' => [0.8, 0.2] })
expect(color).to be_a(Hue::Colors::XY)
expect(color.x).to eq(0.8)
expect(color.y).to eq(0.2)
end
it 'throws error if no mode is passed' do
- expect {
+ expect do
described_class.parse_state({})
- }.to raise_error(Hue::Error, /Unknown or missing state/)
+ end.to raise_error(Hue::Error, /Unknown or missing state/)
end
end
context 'when #parse is used' do
it 'can return a ColorTemperature with single value' do
- color = described_class.parse("5000")
+ color = described_class.parse('5000')
expect(color).to be_a(Hue::Colors::ColorTemperature)
expect(color.kelvin).to eq(5000)
@@ -40,43 +41,43 @@ describe Hue::Colors do
expect(color).to be_a(Hue::Colors::ColorTemperature)
expect(color.mired).to eq(Hue::Colors::ColorTemperature::MIRED_MIN)
- color = described_class.parse("100%")
+ color = described_class.parse('100%')
expect(color).to be_a(Hue::Colors::ColorTemperature)
expect(color.mired).to eq(Hue::Colors::ColorTemperature::MIRED_MAX)
end
it 'can return HueSaturation' do
- color = described_class.parse(*[3e4, 200])
+ color = described_class.parse(3e4, 200)
expect(color).to be_a(Hue::Colors::HueSaturation)
expect(color.hue).to eq(3e4)
expect(color.sat).to eq(200)
- color = described_class.parse(*['20%', 150])
+ color = described_class.parse('20%', 150)
expect(color).to be_a(Hue::Colors::HueSaturation)
- expect(color.hue).to eq(13107)
+ expect(color.hue).to eq(13_107)
expect(color.sat).to eq(150)
- color = described_class.parse(*[7e4, '100%'])
+ color = described_class.parse(7e4, '100%')
expect(color).to be_a(Hue::Colors::HueSaturation)
expect(color.hue).to eq(Hue::Colors::HueSaturation::HUE_MAX)
expect(color.sat).to eq(Hue::Colors::HueSaturation::SATURATION_MAX)
end
it 'can return XY' do
- color = described_class.parse(*[0.2, 0.5])
+ color = described_class.parse(0.2, 0.5)
expect(color).to be_a(Hue::Colors::XY)
expect(color.x).to eq(0.2)
expect(color.y).to eq(0.5)
end
it 'can return RGB' do
- color = described_class.parse(*[0.0, 300, 155])
+ color = described_class.parse(0.0, 300, 155)
expect(color).to be_a(Hue::Colors::RGB)
expect(color.red).to eq(0)
expect(color.green).to eq(255)
expect(color.blue).to eq(155)
- color = described_class.parse(*['10%', '100%', '50%'])
+ color = described_class.parse('10%', '100%', '50%')
expect(color).to be_a(Hue::Colors::RGB)
expect(color.red).to eq(26)
expect(color.green).to eq(255)
@@ -84,9 +85,9 @@ describe Hue::Colors do
end
it 'throws error if no mode is passed' do
- expect {
+ expect do
described_class.parse
- }.to raise_error(Hue::Error, /Unable to parse to color:/)
+ end.to raise_error(Hue::Error, /Unable to parse to color:/)
end
end
end
diff --git a/hue/spec/hue/config/abstract_spec.rb b/hue/spec/hue/config/abstract_spec.rb
@@ -1,14 +1,20 @@
-require 'spec_helper.rb'
+# frozen_string_literal: true
-class Hue::Config::AbstractTest < Hue::Config::Abstract
- private
- def add_self_to_yaml(yaml)
- yaml[name] = { 1 => :test}
+require 'spec_helper'
+
+module Hue
+ module Config
+ class AbstractTest < Hue::Config::Abstract
+ private
+
+ def add_self_to_yaml(yaml)
+ yaml[name] = { 1 => :test }
+ end
+ end
end
end
describe Hue::Config::Abstract do
-
before(:all) do
create_test_application_config
end
@@ -16,17 +22,17 @@ describe Hue::Config::Abstract do
context 'given an new config' do
let(:config) { described_class.new('test', TEST_CONFIG_APPLICATION_PATH) }
- it 'should report the values' do
+ it 'reports the values' do
expect(config.name).to eq('test')
expect(config.path).to eq(TEST_CONFIG_APPLICATION_PATH)
end
- it 'should allow writing the new config to file' do
+ it 'allows writing the new config to file' do
config.write
expect(YAML.load_file(config.path)['test']).to be_a(Hash)
end
- it 'should allow deleting that named config from the file' do
+ it 'allows deleting that named config from the file' do
config.delete
expect(YAML.load_file(config.path)['test']).to be_nil
end
@@ -35,11 +41,10 @@ describe Hue::Config::Abstract do
context 'given an existing config' do
let(:config) { described_class.new('test', EMPTY_CONFIG_FILE) }
- it 'should allow writing to the file' do
+ it 'allows writing to the file' do
config.write
expect(YAML.load_file(config.path)['test']).to be_a(Hash)
config.delete
end
end
-
end
diff --git a/hue/spec/hue/config/application_spec.rb b/hue/spec/hue/config/application_spec.rb
@@ -1,8 +1,9 @@
+# frozen_string_literal: true
+
require 'spec_helper'
describe Hue::Config::Application do
-
- before(:each) do
+ before do
mock_application_config_path
end
@@ -10,26 +11,26 @@ describe Hue::Config::Application do
create_test_application_config
end
- it 'should report the config file location' do
+ it 'reports the config file location' do
expect(described_class.file_path).to eq(TEST_CONFIG_APPLICATION_PATH)
end
- it "should throw and error if a named config doesn't exist" do
- expect {
+ it "throws and error if a named config doesn't exist" do
+ expect do
described_class.named('not_default')
- }.to raise_error(Hue::Config::NotFound, /Config named (.*) not found/)
+ end.to raise_error(Hue::Config::NotFound, /Config named (.*) not found/)
end
context 'with a config file, containing a default' do
let(:config) { described_class.default }
- it "should give the default config and report it's values" do
+ it "gives the default config and report it's values" do
expect(config.name).to eq(described_class::STRING_DEFAULT)
expect(config.bridge_id).to eq(TEST_CONFIG_APPLICATION[config.name][described_class::STRING_BRIDGE_ID])
expect(config.id).to eq(TEST_CONFIG_APPLICATION[config.name][described_class::STRING_ID])
end
- it 'should allow deleting the default config from the file' do
+ it 'allows deleting the default config from the file' do
config.delete
expect(YAML.load_file(described_class.file_path)[described_class::STRING_DEFAULT]).to be_nil
end
@@ -38,37 +39,36 @@ describe Hue::Config::Application do
context 'given an new config' do
let(:config) { described_class.new('http://someip/api', 'some_id', 'not_default') }
- it 'should report the values' do
+ it 'reports the values' do
expect(config.name).to eq('not_default')
expect(config.bridge_id).to eq('http://someip/api')
expect(config.id).to eq('some_id')
end
- it 'should allow writing the new config to file' do
+ it 'allows writing the new config to file' do
config.write
expect(YAML.load_file(described_class.file_path)['not_default']).to be_a(Hash)
end
- it 'should allow fetching that name config' do
+ it 'allows fetching that name config' do
+ config.write
named_config = described_class.named('not_default')
expect(named_config).to eq(config)
end
- it 'should allow deleting that named config from the file' do
+ it 'allows deleting that named config from the file' do
config.delete
expect(YAML.load_file(described_class.file_path)['not_default']).to be_nil
end
end
context 'given an non existing config' do
-
- it "should throw and error if a named config doesn't exist" do
+ it "throws and error if a named config doesn't exist" do
expect(described_class).to receive(:file_path).and_return(EMPTY_CONFIG_FILE).once
- expect {
+ expect do
described_class.named('not_default')
- }.to raise_error(Hue::Config::NotFound, /Config named (.*) not found/)
+ end.to raise_error(Hue::Config::NotFound, /Config named (.*) not found/)
end
end
-
end
diff --git a/hue/spec/hue/config/bridge_spec.rb b/hue/spec/hue/config/bridge_spec.rb
@@ -1,8 +1,9 @@
-require 'spec_helper.rb'
+# frozen_string_literal: true
-describe Hue::Config::Bridge do
+require 'spec_helper'
- before(:each) do
+describe Hue::Config::Bridge do
+ before do
mock_bridge_config_path
end
@@ -10,11 +11,11 @@ describe Hue::Config::Bridge do
create_test_bridge_config
end
- it 'should report the config file location' do
+ it 'reports the config file location' do
expect(described_class.file_path).to eq(TEST_CONFIG_BRIDGE_PATH)
end
- it 'should find existing bridges given an id' do
+ it 'finds existing bridges given an id' do
found = described_class.find(TEST_BRIDGE_UUID)
expect(found.id).to eq(TEST_BRIDGE_UUID)
expect(found.uri).to eq(TEST_BRIDGE_URI)
@@ -27,11 +28,10 @@ describe Hue::Config::Bridge do
let(:uri) { 'http://someip/api' }
let(:config) { described_class.new(uuid, uri) }
- it 'should report the values' do
+ it 'reports the values' do
expect(config.name).to eq(uuid)
expect(config.id).to eq(uuid)
expect(config.uri).to eq(uri)
end
end
-
end
diff --git a/hue/spec/hue/extensions/bridge_spec.rb b/hue/spec/hue/extensions/bridge_spec.rb
@@ -0,0 +1,58 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Hue::Bridge, 'output extensions' do
+ let(:bridge) { test_bridge }
+
+ before { Indentation.reset }
+
+ describe '#print_config' do
+ it 'prints application id and bridge URI' do
+ out = with_stdout { bridge.print_config }
+ expect(out).to include('Config:')
+ expect(out).to include("Application id: #{TEST_APPLICATION_UUID}")
+ expect(out).to include("Bridge URI: #{TEST_BRIDGE_URI}")
+ end
+ end
+
+ describe '#print_state' do
+ it 'prints basic info, network config, applications and lights' do
+ with_fake_request
+ out = with_stdout { bridge.print_state }
+ expect(out).to include('Philips hue')
+ expect(out).to include('Button pressed:')
+ expect(out).to include('Network configuration:')
+ expect(out).to include('IP:')
+ expect(out).to include('Applications:')
+ expect(out).to include('Lights:')
+ expect(out).to include('Living')
+ expect(out).to include('Bedroom Far')
+ end
+
+ it 'prints proxy info when proxyaddress is set' do
+ proxy_state = JSON.parse(File.read(File.join(TestHelpers::JSON_FIXTURE_DIR, 'get_success.json')))
+ proxy_state['config']['proxyaddress'] = 'proxy.example'
+ proxy_state['config']['proxyport'] = 8080
+ stub_request(:get, "#{TEST_BRIDGE_URI}/#{TEST_APPLICATION_UUID}")
+ .to_return(body: proxy_state.to_json, status: 200,
+ headers: { 'Content-Type' => 'application/json' })
+
+ out = with_stdout { bridge.print_state }
+ expect(out).to include('Proxy: proxy.example:8080')
+ end
+ end
+
+ describe '#print_bulbs' do
+ it 'iterates and prints each bulb' do
+ with_fake_request(:lights)
+ stub_request(:get, %r{#{TEST_BRIDGE_URI}/#{TEST_APPLICATION_UUID}/lights/\d+})
+ .to_return(body: File.read(File.join(TestHelpers::JSON_FIXTURE_DIR, 'lights/1.json')),
+ status: 200, headers: { 'Content-Type' => 'application/json' })
+
+ out = with_stdout { bridge.print_bulbs }
+ expect(out).to include('Living')
+ expect(out).to include('Brightness:')
+ end
+ end
+end
diff --git a/hue/spec/hue/extensions/bulb_spec.rb b/hue/spec/hue/extensions/bulb_spec.rb
@@ -0,0 +1,49 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Hue::Bulb, 'output extensions' do
+ let(:bulb) { described_class.new(test_bridge, 1) }
+
+ before do
+ Indentation.reset
+ with_fake_request('lights/1')
+ bulb.refresh!
+ end
+
+ describe '#print_state' do
+ it 'prints the bulb id, name and OFF status' do
+ out = with_stdout { bulb.print_state }
+ expect(out).to include('1.')
+ expect(out).to include('Living')
+ expect(out).to include('OFF')
+ expect(out).to include('Brightness:')
+ end
+
+ it 'prints color when hue is present' do
+ out = with_stdout { bulb.print_state }
+ expect(out).to include('Color:')
+ end
+
+ it 'prints transitory state when blinking' do
+ with_fake_update('lights/1/state', alert: 'lselect')
+ bulb.blink
+ out = with_stdout { bulb.print_state }
+ expect(out).to include('Blinking!')
+ end
+
+ it 'prints transitory state when an effect is set' do
+ with_fake_update('lights/1/state', effect: 'colorloop')
+ bulb.colorloop
+ out = with_stdout { bulb.print_state }
+ expect(out).to include('Effect:')
+ end
+
+ it 'prints ON when bulb is on' do
+ with_fake_update('lights/1/state', on: true)
+ bulb.on
+ out = with_stdout { bulb.print_state }
+ expect(out).to include('ON')
+ end
+ end
+end
diff --git a/hue/spec/hue/utilities/indentation_spec.rb b/hue/spec/hue/utilities/indentation_spec.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Indentation do
+ let(:host) { Class.new { include Indentation }.new }
+
+ before { described_class.reset }
+
+ it 'starts at level 0 with no size' do
+ expect(described_class.indentation_level).to eq(0)
+ expect(described_class.indentation_size).to eq(0)
+ end
+
+ it 'indents inside a block and restores on exit' do
+ with_stdout do
+ host.puts 'a'
+ host.indent(2) { host.puts 'b' }
+ host.puts 'c'
+ end.tap do |out|
+ expect(out).to eq("a\n b\nc\n")
+ end
+ end
+
+ it 'restores level even if the block raises' do
+ expect { host.indent(2) { raise 'boom' } }.to raise_error('boom')
+ expect(described_class.indentation_level).to eq(0)
+ end
+
+ it 'nests indentation' do
+ out = with_stdout do
+ host.indent(2) do
+ host.puts 'x'
+ host.indent(2) { host.puts 'y' }
+ end
+ end
+ expect(out).to eq(" x\n y\n")
+ end
+end
diff --git a/hue/spec/hue_spec.rb b/hue/spec/hue_spec.rb
@@ -1,36 +1,35 @@
-require 'spec_helper.rb'
+# frozen_string_literal: true
+
+require 'spec_helper'
describe Hue do
- before(:each) do
+ before do
mock_application_config_path
mock_bridge_config_path
+ mock_udp_replies
end
- after(:each) do
+ after do
create_test_bridge_config
end
- before(:each) do
- mock_udp_replies
- end
-
- it 'should report the device type as itself' do
+ it 'reports the device type as itself' do
expect(described_class.device_type).to eq(described_class::DEVICE_TYPE)
end
- it 'should return the default application' do
+ it 'returns the default application' do
expect(described_class.application).to be_a(described_class::Bridge)
end
context 'when discovering a bridge on the network' do
- it 'should return a list discovered bridges' do
- bridges = Hue.discover
- expect(bridges).to eq({TEST_UDP_BRIDGE_UUID => TEST_UDP_BRIDGE_URI})
+ it 'returns a list discovered bridges' do
+ bridges = described_class.discover
+ expect(bridges).to eq({ TEST_UDP_BRIDGE_UUID => TEST_UDP_BRIDGE_URI })
end
- it 'should allow registering of discovered bridges' do
+ it 'allows registering of discovered bridges' do
expect(Hue::Config::Bridge.find(TEST_UDP_BRIDGE_UUID)).to be_nil
- registered = Hue.register_bridges
+ registered = described_class.register_bridges
new_bridge = registered[TEST_UDP_BRIDGE_UUID]
expect(new_bridge.id).to eq(TEST_UDP_BRIDGE_UUID)
expect(new_bridge.uri).to eq(TEST_UDP_BRIDGE_URI)
@@ -38,27 +37,27 @@ describe Hue do
end
context 'when attempting discover without a bridge on the network' do
- before(:each) do
+ before do
mock_udp_no_reply
mock_nupnp_empty_reply
end
- it 'should return an empty list of bridges' do
- bridges = Hue.discover
+ it 'returns an empty list of bridges' do
+ bridges = described_class.discover
expect(bridges).to eq({})
end
end
context 'after discovering bridges' do
- before(:each) do
+ before do
mock_udp_replies(TEST_BRIDGE_UUID, 'new_host')
end
- it 'should update already registered bridges' do
+ it 'updates already registered bridges' do
bridge = Hue::Config::Bridge.find(TEST_BRIDGE_UUID)
expect(bridge).not_to be_nil
- registered = Hue.register_bridges
+ registered = described_class.register_bridges
updated_bridge = registered[TEST_BRIDGE_UUID]
expect(updated_bridge.id).to eq(TEST_BRIDGE_UUID)
@@ -67,13 +66,13 @@ describe Hue do
end
context 'when registering or un-registering the application' do
- it 'should throw and error if the default already exists' do
- expect {
+ it 'throws and error if the default already exists' do
+ expect do
described_class.register_default
- }.to raise_error(described_class::Error, described_class::ERROR_DEFAULT_EXISTS)
+ end.to raise_error(described_class::Error, described_class::ERROR_DEFAULT_EXISTS)
end
- it 'should allow a new default if one doesn\'t exist' do
+ it "allows a new default if one doesn't exist" do
with_temp_config_path do
with_fake_post(nil, {}, 'post_success', TEST_UDP_BRIDGE_URI)
with_stdout(/Registering new app...$/) do
@@ -82,7 +81,7 @@ describe Hue do
end
end
- it 'should allow un-registering the default' do
+ it 'allows un-registering the default' do
with_temp_config_path(true) do
with_fake_delete("config/whitelist/#{TEST_APPLICATION_UUID}")
described_class.remove_default
@@ -97,5 +96,4 @@ describe Hue do
expect(described_class.percent_to_unit_interval('100%')).to eq(1.00)
end
end
-
end
diff --git a/hue/spec/spec_helper.rb b/hue/spec/spec_helper.rb
@@ -1,11 +1,22 @@
# frozen_string_literal: true
+require 'simplecov'
+SimpleCov.start do
+ add_filter '/spec/'
+ add_filter '/pkg/'
+ add_filter '/vendor/'
+end
+
require 'hue'
require 'rspec'
require 'webmock/rspec'
+require_relative 'test_helpers'
+
+include TestHelpers # rubocop:disable Style/MixinUsage -- expose constants at top-level for specs
RSpec.configure do |config|
config.mock_with :rspec
+ config.include TestHelpers
end
WebMock.disable_net_connect!
diff --git a/hue/spec/test_helpers.rb b/hue/spec/test_helpers.rb
@@ -0,0 +1,150 @@
+# frozen_string_literal: true
+
+require 'fileutils'
+require 'json'
+require 'stringio'
+require 'tmpdir'
+require 'yaml'
+
+module TestHelpers
+ TEST_BRIDGE_UUID = 'bc6be180-4c57-0130-8d8f-0018de9ecdd0'
+ TEST_BRIDGE_URI = 'http://localhost/api'
+ TEST_APPLICATION_UUID = 'application_uuid'
+
+ TEST_UDP_BRIDGE_UUID = 'aa0be180-4c57-0130-8d8f-0018de9ecdd0'
+ TEST_UDP_BRIDGE_IP = '10.0.0.42'
+ TEST_UDP_BRIDGE_URI = "http://#{TEST_UDP_BRIDGE_IP}/api".freeze
+
+ FIXTURE_DIR = File.expand_path('config', __dir__)
+ JSON_FIXTURE_DIR = File.expand_path('json', __dir__)
+ EMPTY_CONFIG_FILE = File.join(FIXTURE_DIR, 'empty.yml')
+
+ TEST_TMP_DIR = File.join(Dir.tmpdir, 'hue-spec')
+ TEST_CONFIG_APPLICATION_PATH = File.join(TEST_TMP_DIR, 'applications.yml')
+ TEST_CONFIG_BRIDGE_PATH = File.join(TEST_TMP_DIR, 'bridges.yml')
+
+ TEST_CONFIG_APPLICATION = YAML.load_file(File.join(FIXTURE_DIR, 'applications.yml'))
+ TEST_CONFIG_BRIDGE = YAML.load_file(File.join(FIXTURE_DIR, 'bridges.yml'))
+
+ def mock_application_config_path
+ FileUtils.mkdir_p(TEST_TMP_DIR)
+ create_test_application_config
+ allow(Hue::Config::Application).to receive(:file_path).and_return(TEST_CONFIG_APPLICATION_PATH)
+ end
+
+ def mock_bridge_config_path
+ FileUtils.mkdir_p(TEST_TMP_DIR)
+ create_test_bridge_config
+ allow(Hue::Config::Bridge).to receive(:file_path).and_return(TEST_CONFIG_BRIDGE_PATH)
+ end
+
+ def create_test_application_config
+ FileUtils.mkdir_p(TEST_TMP_DIR)
+ File.write(TEST_CONFIG_APPLICATION_PATH, YAML.dump(TEST_CONFIG_APPLICATION))
+ end
+
+ def create_test_bridge_config
+ FileUtils.mkdir_p(TEST_TMP_DIR)
+ File.write(TEST_CONFIG_BRIDGE_PATH, YAML.dump(TEST_CONFIG_BRIDGE))
+ end
+
+ def mock_udp_replies(uuid = TEST_UDP_BRIDGE_UUID, ip = TEST_UDP_BRIDGE_IP)
+ fake_socket = instance_double(UDPSocket, send: 0)
+ payload = "HTTP/1.1 200 OK\r\nLOCATION: http://#{ip}/description.xml\r\n" \
+ "ST: urn:schemas-upnp-org:device:Basic:1\r\nUSN: uuid:#{uuid}::IpBridge\r\n\r\n"
+ delivered = false
+ allow(fake_socket).to receive(:recvfrom) do
+ raise Hue::Error, 'simulated timeout' if delivered
+
+ delivered = true
+ [payload, [nil, 1900, 'fake.local', ip]]
+ end
+ allow(UDPSocket).to receive(:new).and_return(fake_socket)
+ end
+
+ def mock_udp_no_reply
+ fake_socket = instance_double(UDPSocket, send: 0)
+ allow(fake_socket).to receive(:recvfrom) { raise Hue::Error, 'simulated timeout' }
+ allow(UDPSocket).to receive(:new).and_return(fake_socket)
+ end
+
+ def mock_nupnp_empty_reply
+ stub_request(:get, 'https://www.meethue.com/api/nupnp').to_return(body: '[]', status: 200)
+ end
+
+ def mock_socket_hostname
+ allow(Socket).to receive(:gethostname).and_return('hostname')
+ end
+
+ def with_fake_request(path = nil, kind = :get_success)
+ fixture = kind == :get_success ? (path || :get_success) : kind
+ stub_request(:get, bridge_request_url(TEST_BRIDGE_URI, path))
+ .to_return(body: read_json_fixture(fixture), status: 200,
+ headers: { 'Content-Type' => 'application/json' })
+ end
+
+ def with_fake_post(path = nil, body = nil, fixture = 'post_success', host = TEST_BRIDGE_URI)
+ stub = stub_request(:post, raw_request_url(host, path))
+ stub = stub.with(body: body) if body
+ stub.to_return(body: read_json_fixture(fixture), status: 200,
+ headers: { 'Content-Type' => 'application/json' })
+ end
+
+ def with_fake_delete(path = nil, fixture = 'put_success')
+ stub_request(:delete, bridge_request_url(TEST_BRIDGE_URI, path))
+ .to_return(body: read_json_fixture(fixture), status: 200,
+ headers: { 'Content-Type' => 'application/json' })
+ end
+
+ def with_fake_update(path = nil, body = nil, fixture = 'put_success')
+ stub = stub_request(:put, bridge_request_url(TEST_BRIDGE_URI, path))
+ stub = stub.with(body: body) if body
+ stub.to_return(body: read_json_fixture(fixture), status: 200,
+ headers: { 'Content-Type' => 'application/json' })
+ end
+
+ def api_reply(name)
+ JSON.parse(read_json_fixture(name))
+ end
+
+ def with_temp_config_path(with_default = false)
+ File.write(TEST_CONFIG_APPLICATION_PATH, YAML.dump(with_default ? TEST_CONFIG_APPLICATION : {}))
+ yield
+ ensure
+ create_test_application_config
+ end
+
+ def with_stdout(matcher = nil)
+ original = $stdout
+ $stdout = StringIO.new
+ yield
+ output = $stdout.string
+ expect(output).to match(matcher) if matcher
+ output
+ ensure
+ $stdout = original
+ end
+
+ def test_bridge
+ Hue::Bridge.new(TEST_APPLICATION_UUID, TEST_BRIDGE_URI)
+ end
+
+ private
+
+ def read_json_fixture(name)
+ File.read(File.join(JSON_FIXTURE_DIR, "#{name}.json"))
+ end
+
+ def bridge_request_url(host, path)
+ base = host.sub(%r{/$}, '')
+ segments = [base]
+ segments << TEST_APPLICATION_UUID unless path.to_s.start_with?(TEST_APPLICATION_UUID)
+ segments << path.to_s if path && !path.to_s.empty?
+ segments.reject { |s| s.to_s.empty? }.join('/')
+ end
+
+ def raw_request_url(host, path)
+ base = host.sub(%r{/$}, '')
+ path && !path.to_s.empty? ? "#{base}/#{path}" : base
+ end
+end