state_spec.rb 15 KB

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