| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476 |
- require 'api_client'
- RSpec.describe 'Transitions' do
- before(:all) do
- @client = ApiClient.from_environment
- @client.upload_json('/settings', 'settings.json')
- @transition_params = {
- field: 'level',
- start_value: 0,
- end_value: 100,
- duration: 2.0,
- period: 400
- }
- @num_transition_updates = (@transition_params[:duration]*1000)/@transition_params[:period]
- end
- before(:each) do
- mqtt_params = mqtt_parameters()
- @updates_topic = mqtt_params[:updates_topic]
- @topic_prefix = mqtt_topic_prefix()
- @client.put(
- '/settings',
- mqtt_params.merge(
- mqtt_update_topic_pattern: "#{@topic_prefix}updates/:device_id/:device_type/:group_id"
- )
- )
- @id_params = {
- id: @client.generate_id,
- type: 'rgb_cct',
- group_id: 1
- }
- @client.delete_state(@id_params)
- @mqtt_client = create_mqtt_client()
- # Delete any existing transitions
- @client.get('/transitions')['transitions'].each do |t|
- @client.delete("/transitions/#{t['id']}")
- end
- end
- context 'REST routes' do
- it 'should respond with an empty list when there are no transitions' do
- response = @client.transitions
- expect(response).to eq([])
- end
- it 'should respond with an error when missing parameters for POST /transitions' do
- expect { @client.post('/transitions', {}) }.to raise_error(Net::HTTPServerException)
- end
- it 'should create a new transition with a valid POST /transitions request' do
- response = @client.schedule_transition(@id_params, @transition_params)
- expect(response['success']).to eq(true)
- end
- it 'should list active transitions' do
- @client.schedule_transition(@id_params, @transition_params)
- response = @client.transitions
- expect(response.length).to be >= 1
- end
- it 'should support getting an active transition with GET /transitions/:id' do
- @client.schedule_transition(@id_params, @transition_params)
- response = @client.transitions
- detail_response = @client.get("/transitions/#{response.last['id']}")
- expect(detail_response['period']).to_not eq(nil)
- end
- it 'should support deleting active transitions with DELETE /transitions/:id' do
- @client.schedule_transition(@id_params, @transition_params)
- response = @client.transitions
- response.each do |transition|
- @client.delete("/transitions/#{transition['id']}")
- end
- after_delete_response = @client.transitions
- expect(response.length).to eq(1)
- expect(after_delete_response.length).to eq(0)
- end
- end
- context '"transition" key in state update' do
- it 'should create a new transition' do
- @client.patch_state({status: 'ON', level: 0}, @id_params)
- @client.patch_state({level: 100, transition: 2.0}, @id_params)
- response = @client.transitions
- expect(response.length).to be > 0
- expect(response.last['type']).to eq('field')
- expect(response.last['field']).to eq('level')
- expect(response.last['end_value']).to eq(100)
- @client.delete("/transitions/#{response.last['id']}")
- end
- it 'should transition field' do
- seen_updates = 0
- last_value = nil
- @client.patch_state({status: 'ON', level: 0}, @id_params)
- @mqtt_client.on_update(@id_params) do |id, msg|
- if msg.include?('brightness')
- seen_updates += 1
- last_value = msg['brightness']
- end
- last_value == 255
- end
- @client.patch_state({level: 100, transition: 2.0}, @id_params)
- @mqtt_client.wait_for_listeners
- expect(last_value).to eq(255)
- expect(seen_updates).to eq(8) # duration of 2000ms / 300ms period + 1 for initial packet
- end
- it 'should transition a field downwards' do
- seen_updates = 0
- last_value = nil
- @client.patch_state({status: 'ON'}, @id_params)
- @client.patch_state({level: 100}, @id_params)
- @mqtt_client.on_update(@id_params) do |id, msg|
- if msg.include?('brightness')
- seen_updates += 1
- last_value = msg['brightness']
- end
- last_value == 0
- end
- @client.patch_state({level: 0, transition: 2.0}, @id_params)
- @mqtt_client.wait_for_listeners
- expect(last_value).to eq(0)
- expect(seen_updates).to eq(8) # duration of 2000ms / 300ms period + 1 for initial packet
- end
- it 'should transition two fields at once if received in the same command' do
- updates = {}
- @client.patch_state({status: 'ON', hue: 0, level: 100}, @id_params)
- @mqtt_client.on_update(@id_params) do |id, msg|
- msg.each do |k, v|
- updates[k] ||= []
- updates[k] << v
- end
- updates['hue'] && updates['brightness'] && updates['hue'].last == 250 && updates['brightness'].last == 0
- end
- @client.patch_state({level: 0, hue: 250, transition: 2.0}, @id_params)
- @mqtt_client.wait_for_listeners
- expect(updates['hue'].last).to eq(250)
- expect(updates['brightness'].last).to eq(0)
- expect(updates['hue'].length == updates['brightness'].length).to eq(true), "Should have the same number of updates for both fields"
- expect(updates['hue'].length).to eq(8)
- end
- end
- context 'transition packets' do
- it 'should send an initial state packet' do
- seen = false
- @mqtt_client.on_update(@id_params) do |id, message|
- seen = message['brightness'] == 0
- end
- @client.schedule_transition(@id_params, @transition_params)
- @mqtt_client.wait_for_listeners
- expect(seen).to be(true)
- end
- it 'should respect the period parameter' do
- seen_updates = []
- start_time = Time.now
- @mqtt_client.on_update(@id_params) do |id, message|
- seen_updates << message
- message['brightness'] == 255
- end
- @client.schedule_transition(@id_params, @transition_params.merge(duration: 2.0, period: 500))
- @mqtt_client.wait_for_listeners
- expect(seen_updates.map { |x| x['brightness'] }).to eq([0, 64, 128, 191, 255])
- expect((Time.now - start_time)/4).to be >= 0.5 # Don't count the first update
- end
- it 'should support two transitions for different devices at the same time' do
- id1 = @id_params
- id2 = @id_params.merge(type: 'fut089')
- @client.schedule_transition(id1, @transition_params)
- @client.schedule_transition(id2, @transition_params)
- id1_updates = []
- id2_updates = []
- @mqtt_client.on_update do |id, msg|
- if id[:type] == id1[:type]
- id1_updates << msg
- else
- id2_updates << msg
- end
- id1_updates.length == @num_transition_updates && id2_updates.length == @num_transition_updates
- end
- @mqtt_client.wait_for_listeners
- expect(id1_updates.length).to eq(@num_transition_updates)
- expect(id2_updates.length).to eq(@num_transition_updates)
- end
- it 'should assume initial state if one is not provided' do
- @client.patch_state({status: 'ON', level: 0}, @id_params)
- seen_updates = []
- @mqtt_client.on_update(@id_params) do |id, message|
- seen_updates << message
- message['brightness'] == 255
- end
- @client.schedule_transition(@id_params, @transition_params.reject { |x| x == :start_value }.merge(duration: 2, period: 500))
- @mqtt_client.wait_for_listeners
- expect(seen_updates.map { |x| x['brightness'] }).to eq([0, 64, 128, 191, 255])
- end
- end
- context 'status transition' do
- it 'should transition from off -> on' do
- seen_updates = {}
- @client.patch_state({status: 'OFF'}, @id_params)
- @mqtt_client.on_update(@id_params) do |id, message|
- message.each do |k, v|
- seen_updates[k] ||= []
- seen_updates[k] << v
- end
- seen_updates['brightness'] && seen_updates['brightness'].last == 255
- end
- @client.patch_state({status: 'ON', transition: 1.0}, @id_params)
- @mqtt_client.wait_for_listeners
- expect(seen_updates['state']).to eq(['ON'])
- expect(seen_updates['brightness']).to eq([0, 64, 128, 191, 255])
- end
- it 'should transition from on -> off' do
- seen_updates = {}
- @client.patch_state({status: 'ON', level: 100}, @id_params)
- @mqtt_client.on_update(@id_params) do |id, message|
- message.each do |k, v|
- seen_updates[k] ||= []
- seen_updates[k] << v
- end
- seen_updates['state'] == ['OFF']
- end
- @client.patch_state({status: 'OFF', transition: 1.0}, @id_params)
- @mqtt_client.wait_for_listeners
- expect(seen_updates['state']).to eq(['OFF'])
- expect(seen_updates['brightness']).to eq([255, 191, 128, 64, 0])
- end
- it 'should transition from off -> on with known last brightness' do
- seen_updates = {}
- @client.patch_state({status: 'ON', brightness: 99}, @id_params)
- @client.patch_state({status: 'OFF'}, @id_params)
- @mqtt_client.on_update(@id_params) do |id, message|
- message.each do |k, v|
- seen_updates[k] ||= []
- seen_updates[k] << v
- end
- seen_updates['brightness'] && seen_updates['brightness'].last == 255
- end
- @client.patch_state({status: 'ON', transition: 1.0}, @id_params)
- @mqtt_client.wait_for_listeners
- expect(seen_updates['brightness']).to eq([99, 140, 181, 222, 255])
- end
- it 'should transition from on -> off with known last brightness' do
- seen_updates = {}
- @client.patch_state({status: 'ON', brightness: 99}, @id_params)
- @mqtt_client.on_update(@id_params) do |id, message|
- message.each do |k, v|
- seen_updates[k] ||= []
- seen_updates[k] << v
- end
- seen_updates['state'] == ['OFF']
- end
- @client.patch_state({status: 'OFF', transition: 1.0}, @id_params)
- @mqtt_client.wait_for_listeners
- expect(seen_updates['brightness']).to eq([99, 74, 48, 23, 0])
- end
- end
- context 'field support' do
- {
- 'level' => {range: [0, 100], update_field: 'brightness', update_max: 255},
- 'brightness' => {range: [0, 255]},
- 'kelvin' => {range: [0, 100], update_field: 'color_temp', update_min: 153, update_max: 370},
- 'color_temp' => {range: [153, 370]},
- 'hue' => {range: [0, 359]},
- 'saturation' => {range: [0, 100]}
- }.each do |field, params|
- min, max = params[:range]
- update_min = params[:update_min] || min
- update_max = params[:update_max] || max
- update_field = params[:update_field] || field
- it "should support field '#{field}' min --> max" do
- seen_updates = []
- @client.patch_state({'status' => 'ON', field => min}, @id_params)
- @mqtt_client.on_update(@id_params) do |id, message|
- seen_updates << message
- message[update_field] == update_max
- end
- @client.patch_state({field => max, 'transition' => 1.0}, @id_params)
- @mqtt_client.wait_for_listeners
- expect(seen_updates.length).to eq(5)
- expect(seen_updates.last[update_field]).to eq(update_max)
- end
- it "should support field '#{field}' max --> min" do
- seen_updates = []
- @client.patch_state({'status' => 'ON', field => max}, @id_params)
- @mqtt_client.on_update(@id_params) do |id, message|
- seen_updates << message
- message[update_field] == update_min
- end
- @client.patch_state({field => min, 'transition' => 1.0}, @id_params)
- @mqtt_client.wait_for_listeners
- expect(seen_updates.length).to eq(5)
- expect(seen_updates.last[update_field]).to eq(update_min)
- end
- end
- end
- context 'color support' do
- it 'should support color transitions' do
- response = @client.schedule_transition(@id_params, {
- field: 'color',
- start_value: '255,0,0',
- end_value: '0,255,0',
- duration: 1.0,
- period: 500
- })
- expect(response['success']).to eq(true)
- end
- it 'should smoothly transition from one color to another' do
- seen_updates = []
- fields = @client.get('/settings')['group_state_fields']
- @client.put(
- '/settings',
- group_state_fields: fields + %w(oh_color),
- mqtt_state_rate_limit: 1000
- )
- @mqtt_client.on_state(@id_params) do |id, message|
- color = message['color']
- seen_updates << color
- color == '0,255,0'
- end
- response = @client.schedule_transition(@id_params, {
- field: 'color',
- start_value: '255,0,0',
- end_value: '0,255,0',
- duration: 4.0,
- period: 1000
- })
- @mqtt_client.wait_for_listeners
- parts = seen_updates.map { |x| x.split(',').map(&:to_i) }
- # This is less even than you'd expect because RGB -> Hue/Sat is lossy.
- # Raw logs show that the right thing is happening:
- #
- # >>> stepSizes = (-64,64,0)
- # >>> start = (255,0,0)
- # >>> end = (0,255,0)
- # >>> current color = (191,64,0)
- # >>> current color = (127,128,0)
- # >>> current color = (63,192,0)
- # >>> current color = (0,255,0)
- expect(parts).to eq([
- [255, 0, 0],
- [255, 84, 0],
- [250, 255, 0],
- [84, 255, 0],
- [0, 255, 0]
- ])
- end
- it 'should handle color transitions from known state' do
- seen_updates = []
- fields = @client.get('/settings')['group_state_fields']
- @client.put(
- '/settings',
- group_state_fields: fields + %w(oh_color),
- mqtt_state_rate_limit: 1000
- )
- @client.patch_state({status: 'ON', color: '255,0,0'}, @id_params)
- @mqtt_client.on_state(@id_params) do |id, message|
- color = message['color']
- seen_updates << color if color
- color == '0,0,255'
- end
- @client.patch_state({color: '0,0,255', transition: 2.0}, @id_params)
- @mqtt_client.wait_for_listeners
- parts = seen_updates.map { |x| x.split(',').map(&:to_i) }
- expect(parts).to eq([
- [255,0,0],
- [161,0,255],
- [0,0,255]
- ])
- end
- end
- end
|