transition_spec.rb 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378
  1. require 'api_client'
  2. RSpec.describe 'Transitions' do
  3. before(:all) do
  4. @client = ApiClient.from_environment
  5. @client.upload_json('/settings', 'settings.json')
  6. @transition_params = {
  7. field: 'level',
  8. start_value: 0,
  9. end_value: 100,
  10. duration: 2.0,
  11. period: 400
  12. }
  13. @num_transition_updates = (@transition_params[:duration]*1000)/@transition_params[:period]
  14. end
  15. before(:each) do
  16. mqtt_params = mqtt_parameters()
  17. @updates_topic = mqtt_params[:updates_topic]
  18. @topic_prefix = mqtt_topic_prefix()
  19. @client.put(
  20. '/settings',
  21. mqtt_params.merge(
  22. mqtt_update_topic_pattern: "#{@topic_prefix}updates/:device_id/:device_type/:group_id"
  23. )
  24. )
  25. @id_params = {
  26. id: @client.generate_id,
  27. type: 'rgb_cct',
  28. group_id: 1
  29. }
  30. @client.delete_state(@id_params)
  31. @mqtt_client = create_mqtt_client()
  32. # Delete any existing transitions
  33. @client.get('/transitions')['transitions'].each do |t|
  34. @client.delete("/transitions/#{t['id']}")
  35. end
  36. end
  37. context 'REST routes' do
  38. it 'should respond with an empty list when there are no transitions' do
  39. response = @client.transitions
  40. expect(response).to eq([])
  41. end
  42. it 'should respond with an error when missing parameters for POST /transitions' do
  43. expect { @client.post('/transitions', {}) }.to raise_error(Net::HTTPServerException)
  44. end
  45. it 'should create a new transition with a valid POST /transitions request' do
  46. response = @client.schedule_transition(@id_params, @transition_params)
  47. expect(response['success']).to eq(true)
  48. end
  49. it 'should list active transitions' do
  50. @client.schedule_transition(@id_params, @transition_params)
  51. response = @client.transitions
  52. expect(response.length).to be >= 1
  53. end
  54. it 'should support getting an active transition with GET /transitions/:id' do
  55. @client.schedule_transition(@id_params, @transition_params)
  56. response = @client.transitions
  57. detail_response = @client.get("/transitions/#{response.last['id']}")
  58. expect(detail_response['period']).to_not eq(nil)
  59. end
  60. it 'should support deleting active transitions with DELETE /transitions/:id' do
  61. @client.schedule_transition(@id_params, @transition_params)
  62. response = @client.transitions
  63. response.each do |transition|
  64. @client.delete("/transitions/#{transition['id']}")
  65. end
  66. after_delete_response = @client.transitions
  67. expect(response.length).to eq(1)
  68. expect(after_delete_response.length).to eq(0)
  69. end
  70. end
  71. context '"transition" key in state update' do
  72. it 'should create a new transition' do
  73. @client.patch_state({status: 'ON', level: 0}, @id_params)
  74. @client.patch_state({level: 100, transition: 2.0}, @id_params)
  75. response = @client.transitions
  76. expect(response.length).to be > 0
  77. expect(response.last['type']).to eq('field')
  78. expect(response.last['field']).to eq('level')
  79. expect(response.last['end_value']).to eq(100)
  80. @client.delete("/transitions/#{response.last['id']}")
  81. end
  82. it 'should transition field' do
  83. seen_updates = 0
  84. last_value = nil
  85. @client.patch_state({status: 'ON', level: 0}, @id_params)
  86. @mqtt_client.on_update(@id_params) do |id, msg|
  87. if msg.include?('brightness')
  88. seen_updates += 1
  89. last_value = msg['brightness']
  90. end
  91. last_value == 255
  92. end
  93. @client.patch_state({level: 100, transition: 2.0}, @id_params)
  94. @mqtt_client.wait_for_listeners
  95. expect(last_value).to eq(255)
  96. expect(seen_updates).to eq(8) # duration of 2000ms / 300ms period + 1 for initial packet
  97. end
  98. it 'should transition a field downwards' do
  99. seen_updates = 0
  100. last_value = nil
  101. @client.patch_state({status: 'ON'}, @id_params)
  102. @client.patch_state({level: 100}, @id_params)
  103. @mqtt_client.on_update(@id_params) do |id, msg|
  104. if msg.include?('brightness')
  105. seen_updates += 1
  106. last_value = msg['brightness']
  107. end
  108. last_value == 0
  109. end
  110. @client.patch_state({level: 0, transition: 2.0}, @id_params)
  111. @mqtt_client.wait_for_listeners
  112. expect(last_value).to eq(0)
  113. expect(seen_updates).to eq(8) # duration of 2000ms / 300ms period + 1 for initial packet
  114. end
  115. it 'should transition two fields at once if received in the same command' do
  116. updates = {}
  117. @client.patch_state({status: 'ON', hue: 0, level: 100}, @id_params)
  118. @mqtt_client.on_update(@id_params) do |id, msg|
  119. msg.each do |k, v|
  120. updates[k] ||= []
  121. updates[k] << v
  122. end
  123. updates['hue'] && updates['brightness'] && updates['hue'].last == 250 && updates['brightness'].last == 0
  124. end
  125. @client.patch_state({level: 0, hue: 250, transition: 2.0}, @id_params)
  126. @mqtt_client.wait_for_listeners
  127. expect(updates['hue'].last).to eq(250)
  128. expect(updates['brightness'].last).to eq(0)
  129. expect(updates['hue'].length == updates['brightness'].length).to eq(true), "Should have the same number of updates for both fields"
  130. expect(updates['hue'].length).to eq(8)
  131. end
  132. end
  133. context 'transition packets' do
  134. it 'should send an initial state packet' do
  135. seen = false
  136. @mqtt_client.on_update(@id_params) do |id, message|
  137. seen = message['brightness'] == 0
  138. end
  139. @client.schedule_transition(@id_params, @transition_params)
  140. @mqtt_client.wait_for_listeners
  141. expect(seen).to be(true)
  142. end
  143. it 'should respect the period parameter' do
  144. seen_updates = []
  145. start_time = Time.now
  146. @mqtt_client.on_update(@id_params) do |id, message|
  147. seen_updates << message
  148. message['brightness'] == 255
  149. end
  150. @client.schedule_transition(@id_params, @transition_params.merge(duration: 2.0, period: 500))
  151. @mqtt_client.wait_for_listeners
  152. expect(seen_updates.map { |x| x['brightness'] }).to eq([0, 64, 128, 191, 255])
  153. expect((Time.now - start_time)/4).to be >= 0.5 # Don't count the first update
  154. end
  155. it 'should support two transitions for different devices at the same time' do
  156. id1 = @id_params
  157. id2 = @id_params.merge(type: 'fut089')
  158. @client.schedule_transition(id1, @transition_params)
  159. @client.schedule_transition(id2, @transition_params)
  160. id1_updates = []
  161. id2_updates = []
  162. @mqtt_client.on_update do |id, msg|
  163. if id[:type] == id1[:type]
  164. id1_updates << msg
  165. else
  166. id2_updates << msg
  167. end
  168. id1_updates.length == @num_transition_updates && id2_updates.length == @num_transition_updates
  169. end
  170. @mqtt_client.wait_for_listeners
  171. expect(id1_updates.length).to eq(@num_transition_updates)
  172. expect(id2_updates.length).to eq(@num_transition_updates)
  173. end
  174. end
  175. context 'field support' do
  176. {
  177. 'level' => {range: [0, 100], update_field: 'brightness', update_max: 255},
  178. 'brightness' => {range: [0, 255]},
  179. 'kelvin' => {range: [0, 100], update_field: 'color_temp', update_min: 153, update_max: 370},
  180. 'color_temp' => {range: [153, 370]},
  181. 'hue' => {range: [0, 359]},
  182. 'saturation' => {range: [0, 100]}
  183. }.each do |field, params|
  184. min, max = params[:range]
  185. update_min = params[:update_min] || min
  186. update_max = params[:update_max] || max
  187. update_field = params[:update_field] || field
  188. it "should support field '#{field}' min --> max" do
  189. seen_updates = []
  190. @client.patch_state({'status' => 'ON', field => min}, @id_params)
  191. @mqtt_client.on_update(@id_params) do |id, message|
  192. seen_updates << message
  193. message[update_field] == update_max
  194. end
  195. @client.patch_state({field => max, 'transition' => 1.0}, @id_params)
  196. @mqtt_client.wait_for_listeners
  197. expect(seen_updates.length).to eq(5)
  198. expect(seen_updates.last[update_field]).to eq(update_max)
  199. end
  200. it "should support field '#{field}' max --> min" do
  201. seen_updates = []
  202. @client.patch_state({'status' => 'ON', field => max}, @id_params)
  203. @mqtt_client.on_update(@id_params) do |id, message|
  204. seen_updates << message
  205. message[update_field] == update_min
  206. end
  207. @client.patch_state({field => min, 'transition' => 1.0}, @id_params)
  208. @mqtt_client.wait_for_listeners
  209. expect(seen_updates.length).to eq(5)
  210. expect(seen_updates.last[update_field]).to eq(update_min)
  211. end
  212. end
  213. end
  214. context 'color support' do
  215. it 'should support color transitions' do
  216. response = @client.schedule_transition(@id_params, {
  217. field: 'color',
  218. start_value: '255,0,0',
  219. end_value: '0,255,0',
  220. duration: 1.0,
  221. period: 500
  222. })
  223. expect(response['success']).to eq(true)
  224. end
  225. it 'should smoothly transition from one color to another' do
  226. seen_updates = []
  227. fields = @client.get('/settings')['group_state_fields']
  228. @client.put(
  229. '/settings',
  230. group_state_fields: fields + %w(oh_color),
  231. mqtt_state_rate_limit: 1000
  232. )
  233. @mqtt_client.on_state(@id_params) do |id, message|
  234. color = message['color']
  235. seen_updates << color
  236. color == '0,255,0'
  237. end
  238. response = @client.schedule_transition(@id_params, {
  239. field: 'color',
  240. start_value: '255,0,0',
  241. end_value: '0,255,0',
  242. duration: 4.0,
  243. period: 1000
  244. })
  245. @mqtt_client.wait_for_listeners
  246. parts = seen_updates.map { |x| x.split(',').map(&:to_i) }
  247. # This is less even than you'd expect because RGB -> Hue/Sat is lossy.
  248. # Raw logs show that the right thing is happening:
  249. #
  250. # >>> stepSizes = (-64,64,0)
  251. # >>> start = (255,0,0)
  252. # >>> end = (0,255,0)
  253. # >>> current color = (191,64,0)
  254. # >>> current color = (127,128,0)
  255. # >>> current color = (63,192,0)
  256. # >>> current color = (0,255,0)
  257. expect(parts).to eq([
  258. [255, 0, 0],
  259. [255, 84, 0],
  260. [250, 255, 0],
  261. [84, 255, 0],
  262. [0, 255, 0]
  263. ])
  264. end
  265. it 'should handle color transitions from known state' do
  266. seen_updates = []
  267. fields = @client.get('/settings')['group_state_fields']
  268. @client.put(
  269. '/settings',
  270. group_state_fields: fields + %w(oh_color),
  271. mqtt_state_rate_limit: 1000
  272. )
  273. @client.patch_state({status: 'ON', color: '255,0,0'}, @id_params)
  274. @mqtt_client.on_state(@id_params) do |id, message|
  275. color = message['color']
  276. seen_updates << color if color
  277. color == '0,0,255'
  278. end
  279. @client.patch_state({color: '0,0,255', transition: 2.0}, @id_params)
  280. @mqtt_client.wait_for_listeners
  281. parts = seen_updates.map { |x| x.split(',').map(&:to_i) }
  282. expect(parts).to eq([
  283. [255,0,0],
  284. [161,0,255],
  285. [0,0,255]
  286. ])
  287. end
  288. end
  289. end