state_spec.rb 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524
  1. require 'api_client'
  2. RSpec.describe 'State' do
  3. before(:all) do
  4. @client = ApiClient.new(ENV.fetch('ESPMH_HOSTNAME'), ENV.fetch('ESPMH_TEST_DEVICE_ID_BASE'))
  5. @client.upload_json('/settings', 'settings.json')
  6. end
  7. before(:each) do
  8. @id_params = {
  9. id: @client.generate_id,
  10. type: 'rgb_cct',
  11. group_id: 1
  12. }
  13. @client.delete_state(@id_params)
  14. end
  15. context 'blockOnQueue parameter' do
  16. it 'should not receive state if we don\'t block on the packet queue' do
  17. response = @client.patch_state({status: 'ON'}, @id_params.merge(blockOnQueue: false))
  18. expect(response).to eq({'success' => true})
  19. end
  20. it 'should receive state if we do block on the packet queue' do
  21. response = @client.patch_state({status: 'ON'}, @id_params.merge(blockOnQueue: true))
  22. expect(response).to eq({'status' => 'ON'})
  23. end
  24. end
  25. context 'initial state' do
  26. it 'should assume white mode for device types that are white-only' do
  27. %w(cct fut091).each do |type|
  28. id = @id_params.merge(type: type)
  29. @client.delete_state(id)
  30. state = @client.patch_state({status: 'ON'}, id)
  31. expect(state['bulb_mode']).to eq('white'), "it should assume white mode for #{type}"
  32. end
  33. end
  34. it 'should assume color mode for device types that are rgb-only' do
  35. %w(rgb).each do |type|
  36. id = @id_params.merge(type: type)
  37. @client.delete_state(id)
  38. state = @client.patch_state({status: 'ON'}, id)
  39. expect(state['bulb_mode']).to eq('color'), "it should assume color mode for #{type}"
  40. end
  41. end
  42. end
  43. context 'toggle command' do
  44. it 'should toggle ON to OFF' do
  45. init_state = @client.patch_state({'status' => 'ON'}, @id_params)
  46. expect(init_state['status']).to eq('ON')
  47. next_state = @client.patch_state({'command' => 'toggle'}, @id_params)
  48. expect(next_state['status']).to eq('OFF')
  49. end
  50. it 'should toggle OFF to ON' do
  51. init_state = @client.patch_state({'status' => 'OFF'}, @id_params)
  52. expect(init_state['status']).to eq('OFF')
  53. next_state = @client.patch_state({'command' => 'toggle'}, @id_params)
  54. expect(next_state['status']).to eq('ON')
  55. end
  56. end
  57. context 'night mode command' do
  58. StateHelpers::ALL_REMOTE_TYPES
  59. .reject { |x| %w(rgb).include?(x) } # Night mode not supported for these types
  60. .each do |type|
  61. it "should affect state when bulb is OFF for #{type}" do
  62. params = @id_params.merge(type: type)
  63. @client.delete_state(params)
  64. state = @client.patch_state({'command' => 'night_mode'}, params)
  65. expect(state['bulb_mode']).to eq('night')
  66. expect(state['effect']).to eq('night_mode')
  67. end
  68. end
  69. StateHelpers::ALL_REMOTE_TYPES
  70. .reject { |x| %w(rgb).include?(x) } # Night mode not supported for these types
  71. .each do |type|
  72. it "should affect state when bulb is ON for #{type}" do
  73. params = @id_params.merge(type: type)
  74. @client.delete_state(params)
  75. @client.patch_state({'status' => 'ON'}, params)
  76. state = @client.patch_state({'command' => 'night_mode'}, params)
  77. # RGBW bulbs have to be OFF in order for night mode to take affect
  78. expect(state['status']).to eq('ON') if type != 'rgbw'
  79. expect(state['bulb_mode']).to eq('night')
  80. expect(state['effect']).to eq('night_mode')
  81. end
  82. end
  83. it 'should revert to previous mode when status is toggled' do
  84. @client.patch_state({'status' => 'ON', 'kelvin' => 100}, @id_params)
  85. state = @client.patch_state({'command' => 'night_mode'}, @id_params)
  86. expect(state['effect']).to eq('night_mode')
  87. state = @client.patch_state({'status' => 'OFF'}, @id_params)
  88. expect(state['bulb_mode']).to eq('white')
  89. expect(state['kelvin']).to eq(100)
  90. @client.patch_state({'status' => 'ON', 'hue' => 0}, @id_params)
  91. state = @client.patch_state({'command' => 'night_mode'}, @id_params)
  92. expect(state['effect']).to eq('night_mode')
  93. state = @client.patch_state({'status' => 'OFF'}, @id_params)
  94. expect(state['bulb_mode']).to eq('color')
  95. expect(state['hue']).to eq(0)
  96. end
  97. end
  98. context 'deleting' do
  99. it 'should support deleting state' do
  100. desired_state = {
  101. 'status' => 'ON',
  102. 'level' => 10,
  103. 'hue' => 49,
  104. 'saturation' => 20
  105. }
  106. @client.patch_state(desired_state, @id_params)
  107. resulting_state = @client.get_state(@id_params)
  108. expect(resulting_state).to_not be_empty
  109. @client.delete_state(@id_params)
  110. resulting_state = @client.get_state(@id_params)
  111. expect(resulting_state).to be_empty
  112. end
  113. end
  114. context 'persistence' do
  115. it 'should persist parameters' do
  116. desired_state = {
  117. 'status' => 'ON',
  118. 'level' => 100,
  119. 'hue' => 0,
  120. 'saturation' => 100
  121. }
  122. @client.patch_state(desired_state, @id_params)
  123. patched_state = @client.get_state(@id_params)
  124. states_are_equal(desired_state, patched_state)
  125. desired_state = {
  126. 'status' => 'ON',
  127. 'level' => 10,
  128. 'hue' => 49,
  129. 'saturation' => 20
  130. }
  131. @client.patch_state(desired_state, @id_params)
  132. patched_state = @client.get_state(@id_params)
  133. states_are_equal(desired_state, patched_state)
  134. end
  135. it 'should affect member groups when changing group 0' do
  136. group_0_params = @id_params.merge(group_id: 0)
  137. desired_state = {
  138. 'status' => 'ON',
  139. 'level' => 100,
  140. 'hue' => 0,
  141. 'saturation' => 100
  142. }
  143. @client.patch_state(desired_state, group_0_params)
  144. individual_state = desired_state.merge('level' => 10)
  145. patched_state = @client.patch_state(individual_state, @id_params)
  146. expect(patched_state).to_not eq(desired_state)
  147. states_are_equal(individual_state, patched_state)
  148. group_4_state = @client.get_state(group_0_params.merge(group_id: 4))
  149. states_are_equal(desired_state, group_4_state)
  150. @client.patch_state(desired_state, group_0_params)
  151. group_1_state = @client.get_state(group_0_params.merge(group_id: 1))
  152. states_are_equal(desired_state, group_1_state)
  153. end
  154. it 'should keep group 0 state' do
  155. group_0_params = @id_params.merge(group_id: 0)
  156. desired_state = {
  157. 'status' => 'ON',
  158. 'level' => 100,
  159. 'hue' => 0,
  160. 'saturation' => 100
  161. }
  162. patched_state = @client.patch_state(desired_state, group_0_params)
  163. states_are_equal(desired_state, patched_state)
  164. end
  165. it 'should clear group 0 state after member group state changes' do
  166. group_0_params = @id_params.merge(group_id: 0)
  167. desired_state = {
  168. 'status' => 'ON',
  169. 'level' => 100,
  170. 'kelvin' => 100
  171. }
  172. @client.patch_state(desired_state, group_0_params)
  173. @client.patch_state(desired_state.merge('kelvin' => 10), @id_params)
  174. resulting_state = @client.get_state(group_0_params)
  175. expect(resulting_state.keys).to_not include('kelvin')
  176. states_are_equal(desired_state.reject { |x| x == 'kelvin' }, resulting_state)
  177. end
  178. it 'should not clear group 0 state when updating member group state if value is the same' do
  179. group_0_params = @id_params.merge(group_id: 0)
  180. desired_state = {
  181. 'status' => 'ON',
  182. 'level' => 100,
  183. 'kelvin' => 100
  184. }
  185. @client.patch_state(desired_state, group_0_params)
  186. @client.patch_state(desired_state.merge('kelvin' => 100), @id_params)
  187. resulting_state = @client.get_state(group_0_params)
  188. expect(resulting_state).to include('kelvin')
  189. states_are_equal(desired_state, resulting_state)
  190. end
  191. it 'changing member state mode and then changing level should preserve group 0 brightness for original mode' do
  192. group_0_params = @id_params.merge(group_id: 0)
  193. desired_state = {
  194. 'status' => 'ON',
  195. 'level' => 100,
  196. 'hue' => 0,
  197. 'saturation' => 100
  198. }
  199. @client.delete_state(group_0_params)
  200. @client.patch_state(desired_state, group_0_params)
  201. # color -> white mode. should not have brightness because brightness will
  202. # have been previously unknown to group 0.
  203. @client.patch_state(desired_state.merge('color_temp' => 253, 'level' => 11), @id_params)
  204. resulting_state = @client.get_state(group_0_params)
  205. expect(resulting_state.keys).to_not include('level')
  206. # color -> effect mode. same as above
  207. @client.patch_state(desired_state, group_0_params)
  208. @client.patch_state(desired_state.merge('mode' => 0), @id_params)
  209. resulting_state = @client.get_state(group_0_params)
  210. expect(resulting_state).to_not include('level')
  211. # white mode -> color.
  212. white_mode_desired_state = {'status' => 'ON', 'color_temp' => 253, 'level' => 11}
  213. @client.patch_state(white_mode_desired_state, group_0_params)
  214. @client.patch_state({'hue' => 10}, @id_params)
  215. resulting_state = @client.get_state(group_0_params)
  216. expect(resulting_state).to_not include('level')
  217. @client.patch_state({'hue' => 10}, group_0_params)
  218. resulting_state = @client.get_state(group_0_params)
  219. expect(resulting_state['level']).to eq(100)
  220. # white mode -> effect mode. level never set for group 0, so level should
  221. # level should be present.
  222. @client.patch_state(white_mode_desired_state, group_0_params)
  223. @client.patch_state({'mode' => 0}, @id_params)
  224. resulting_state = @client.get_state(group_0_params)
  225. expect(resulting_state).to_not include('level')
  226. # effect mode -> color. same as white mode -> color
  227. effect_mode_desired_state = {'status' => 'ON', 'mode' => 0, 'level' => 100}
  228. @client.patch_state(effect_mode_desired_state, group_0_params)
  229. @client.patch_state({'hue' => 10}, @id_params)
  230. resulting_state = @client.get_state(group_0_params)
  231. expect(resulting_state).to_not include('level')
  232. # effect mode -> white
  233. @client.patch_state(effect_mode_desired_state, group_0_params)
  234. @client.patch_state({'color_temp' => 253}, @id_params)
  235. resulting_state = @client.get_state(group_0_params)
  236. expect(resulting_state).to_not include('level')
  237. end
  238. end
  239. context 'fields' do
  240. it 'should support on/off' do
  241. @client.patch_state({status: 'on'}, @id_params)
  242. expect(@client.get_state(@id_params)['status']).to eq('ON')
  243. # test "state", which is an alias for "status"
  244. @client.patch_state({state: 'off'}, @id_params)
  245. expect(@client.get_state(@id_params)['status']).to eq('OFF')
  246. end
  247. it 'should support boolean values for status' do
  248. # test boolean value "true", which should be the same as "ON".
  249. @client.patch_state({status: true}, @id_params)
  250. expect(@client.get_state(@id_params)['status']).to eq('ON')
  251. @client.patch_state({state: false}, @id_params)
  252. expect(@client.get_state(@id_params)['status']).to eq('OFF')
  253. end
  254. it 'should support the color field' do
  255. desired_state = {
  256. 'hue' => 0,
  257. 'saturation' => 100,
  258. 'status' => 'ON'
  259. }
  260. @client.patch_state(
  261. desired_state.merge(hue: 100),
  262. @id_params
  263. )
  264. @client.patch_state(
  265. { color: '255,0,0' },
  266. @id_params
  267. )
  268. state = @client.get_state(@id_params)
  269. expect(state.keys).to include(*desired_state.keys)
  270. expect(state.select { |x| desired_state.include?(x) } ).to eq(desired_state)
  271. @client.patch_state(
  272. { color: {r: 0, g: 255, b: 0} },
  273. @id_params
  274. )
  275. state = @client.get_state(@id_params)
  276. desired_state.merge!('hue' => 120)
  277. expect(state.keys).to include(*desired_state.keys)
  278. expect(state.select { |x| desired_state.include?(x) } ).to eq(desired_state)
  279. end
  280. it 'should support separate brightness fields for different modes' do
  281. desired_state = {
  282. 'hue' => 0,
  283. 'level' => 50
  284. }
  285. @client.patch_state(desired_state, @id_params)
  286. result = @client.get_state(@id_params)
  287. expect(result['bulb_mode']).to eq('color')
  288. expect(result['level']).to eq(50)
  289. @client.patch_state({'kelvin' => 100}, @id_params)
  290. @client.patch_state({'level' => 70}, @id_params)
  291. result = @client.get_state(@id_params)
  292. expect(result['bulb_mode']).to eq('white')
  293. expect(result['level']).to eq(70)
  294. @client.patch_state({'hue' => 0}, @id_params)
  295. result = @client.get_state(@id_params)
  296. expect(result['bulb_mode']).to eq('color')
  297. # Should retain previous brightness
  298. expect(result['level']).to eq(50)
  299. end
  300. it 'should support the mode and effect fields' do
  301. state = @client.patch_state({status: 'ON', mode: 0}, @id_params)
  302. expect(state['effect']).to eq("0")
  303. state = @client.patch_state({effect: 1}, @id_params)
  304. expect(state['effect']).to eq("1")
  305. end
  306. end
  307. context 'increment/decrement commands' do
  308. it 'should assume state after sufficiently many down commands' do
  309. id = @id_params.merge(type: 'cct')
  310. @client.delete_state(id)
  311. @client.patch_state({status: 'on'}, id)
  312. expect(@client.get_state(id)).to_not include('brightness', 'kelvin')
  313. 10.times do
  314. @client.patch_state(
  315. { commands: ['level_down', 'temperature_down'] },
  316. id
  317. )
  318. end
  319. state = @client.get_state(id)
  320. expect(state).to include('level', 'kelvin')
  321. expect(state['level']).to eq(0)
  322. expect(state['kelvin']).to eq(0)
  323. end
  324. it 'should assume state after sufficiently many up commands' do
  325. id = @id_params.merge(type: 'cct')
  326. @client.delete_state(id)
  327. @client.patch_state({status: 'on'}, id)
  328. expect(@client.get_state(id)).to_not include('level', 'kelvin')
  329. 10.times do
  330. @client.patch_state(
  331. { commands: ['level_up', 'temperature_up'] },
  332. id
  333. )
  334. end
  335. state = @client.get_state(id)
  336. expect(state).to include('level', 'kelvin')
  337. expect(state['level']).to eq(100)
  338. expect(state['kelvin']).to eq(100)
  339. end
  340. it 'should affect known state' do
  341. id = @id_params.merge(type: 'cct')
  342. @client.delete_state(id)
  343. @client.patch_state({status: 'on'}, id)
  344. expect(@client.get_state(id)).to_not include('level', 'kelvin')
  345. 10.times do
  346. @client.patch_state(
  347. { commands: ['level_up', 'temperature_up'] },
  348. id
  349. )
  350. end
  351. @client.patch_state(
  352. { commands: ['level_down', 'temperature_down'] },
  353. id
  354. )
  355. state = @client.get_state(id)
  356. expect(state).to include('level', 'kelvin')
  357. expect(state['level']).to eq(90)
  358. expect(state['kelvin']).to eq(90)
  359. end
  360. end
  361. context 'state updates while off' do
  362. it 'should not affect persisted state' do
  363. @client.patch_state({'status' => 'OFF'}, @id_params)
  364. state = @client.patch_state({'hue' => 100}, @id_params)
  365. expect(state.count).to eq(1)
  366. expect(state).to include('status')
  367. end
  368. it 'should not affect persisted state using increment/decrement' do
  369. @client.patch_state({'status' => 'OFF'}, @id_params)
  370. 10.times do
  371. @client.patch_state(
  372. { commands: ['level_down', 'temperature_down'] },
  373. @id_params
  374. )
  375. end
  376. state = @client.get_state(@id_params)
  377. expect(state.count).to eq(1)
  378. expect(state).to include('status')
  379. end
  380. end
  381. context 'fut089' do
  382. # FUT089 uses the same command ID for both kelvin and saturation command, so
  383. # interpreting such a command depends on knowledge of the state that the bulb
  384. # is in.
  385. it 'should keep enough group 0 state to interpret ambiguous kelvin/saturation commands as saturation commands when in color mode' do
  386. group0_params = @id_params.merge(type: 'fut089', group_id: 0)
  387. (0..8).each do |group_id|
  388. @client.delete_state(group0_params.merge(group_id: group_id))
  389. end
  390. # Patch in separate commands so state must be kept
  391. @client.patch_state({'status' => 'ON', 'hue' => 0}, group0_params)
  392. @client.patch_state({'saturation' => 100}, group0_params)
  393. (0..8).each do |group_id|
  394. state = @client.get_state(group0_params.merge(group_id: group_id))
  395. expect(state['bulb_mode']).to eq('color')
  396. expect(state['saturation']).to eq(100)
  397. expect(state['hue']).to eq(0)
  398. end
  399. end
  400. end
  401. context 'fut020' do
  402. it 'should support fut020 commands' do
  403. id = @id_params.merge(type: 'fut020', group_id: 0)
  404. @client.delete_state(id)
  405. state = @client.patch_state({status: 'ON'}, id)
  406. expect(state['status']).to eq('ON')
  407. end
  408. it 'should assume the "off" command sets state to on... commands are the same' do
  409. id = @id_params.merge(type: 'fut020', group_id: 0)
  410. @client.delete_state(id)
  411. state = @client.patch_state({status: 'OFF'}, id)
  412. expect(state['status']).to eq('ON')
  413. end
  414. end
  415. end