mqtt_spec.rb 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160
  1. require 'api_client'
  2. require 'mqtt_client'
  3. RSpec.describe 'State' do
  4. before(:all) do
  5. @client = ApiClient.new(ENV.fetch('ESPMH_HOSTNAME'), ENV.fetch('ESPMH_TEST_DEVICE_ID_BASE'))
  6. @client.upload_json('/settings', 'settings.json')
  7. @topic_prefix = ENV.fetch('ESPMH_MQTT_TOPIC_PREFIX')
  8. @updates_topic = "#{@topic_prefix}updates/:device_id/:device_type/:group_id"
  9. @client.put(
  10. '/settings',
  11. mqtt_server: ENV.fetch('ESPMH_MQTT_SERVER'),
  12. mqtt_username: ENV.fetch('ESPMH_MQTT_USERNAME'),
  13. mqtt_password: ENV.fetch('ESPMH_MQTT_PASSWORD'),
  14. mqtt_topic_pattern: "#{@topic_prefix}commands/:device_id/:device_type/:group_id",
  15. mqtt_state_topic_pattern: "#{@topic_prefix}state/:device_id/:device_type/:group_id",
  16. mqtt_update_topic_pattern: @updates_topic
  17. )
  18. @mqtt_client = MqttClient.new(
  19. *%w(SERVER USERNAME PASSWORD).map { |x| ENV.fetch("ESPMH_MQTT_#{x}") } << @topic_prefix
  20. )
  21. end
  22. after(:all) do
  23. @mqtt_client.disconnect
  24. end
  25. before(:each) do
  26. @id_params = {
  27. id: @client.generate_id,
  28. type: 'rgb_cct',
  29. group_id: 1
  30. }
  31. end
  32. context 'birth and LWT' do
  33. # Unfortunately, no way to easily simulate an unclean disconnect, so only test birth
  34. it 'should send birth message when configured' do
  35. birth_topic = "#{@topic_prefix}birth"
  36. @client.put(
  37. '/settings',
  38. mqtt_birth_topic: birth_topic
  39. )
  40. seen_birth = false
  41. @mqtt_client.on_message(birth_topic) do |topic, message|
  42. seen_birth = true
  43. end
  44. # Force MQTT reconnect by updating settings
  45. @client.put('/settings', fakekey: 'fakevalue')
  46. @mqtt_client.wait_for_listeners
  47. expect(seen_birth).to be(true)
  48. end
  49. end
  50. context 'commands and state' do
  51. # Check state using HTTP
  52. it 'should affect state' do
  53. @client.patch_state({level: 50, status: 'off'}, @id_params)
  54. @mqtt_client.patch_state(@id_params, status: 'on', level: 70)
  55. state = @client.get_state(@id_params)
  56. expect(state.keys).to include(*%w(level status))
  57. expect(state['status']).to eq('ON')
  58. expect(state['level']).to eq(70)
  59. end
  60. it 'should publish to state topics' do
  61. desired_state = {'status' => 'ON', 'level' => 80}
  62. seen_state = false
  63. @client.patch_state({status: 'off'}, @id_params)
  64. @mqtt_client.on_state(@id_params) do |id, message|
  65. seen_state = (id == @id_params && desired_state.all? { |k,v| v == message[k] })
  66. end
  67. @mqtt_client.patch_state(@id_params, desired_state)
  68. @mqtt_client.wait_for_listeners
  69. expect(seen_state).to be(true)
  70. end
  71. it 'should publish an update message for each new command' do
  72. tweak_params = {'hue' => 49, 'brightness' => 128, 'saturation' => 50}
  73. desired_state = {'state' => 'ON'}.merge(tweak_params)
  74. init_state = desired_state.merge(Hash[
  75. tweak_params.map do |k, v|
  76. [k, v + 10]
  77. end
  78. ])
  79. @client.patch_state(@id_params, init_state)
  80. accumulated_state = {}
  81. @mqtt_client.on_update(@id_params) do |id, message|
  82. desired_state == accumulated_state.merge!(message)
  83. end
  84. @mqtt_client.patch_state(@id_params, desired_state)
  85. @mqtt_client.wait_for_listeners
  86. expect(accumulated_state).to eq(desired_state)
  87. end
  88. it 'should respect the state update interval' do
  89. # Wait for MQTT to reconnect
  90. @mqtt_client.on_message("#{@topic_prefix}birth") { |x, y| true }
  91. # Disable updates to prevent the negative effects of spamming commands
  92. @client.put(
  93. '/settings',
  94. mqtt_update_topic_pattern: '',
  95. mqtt_state_rate_limit: 500
  96. )
  97. @mqtt_client.wait_for_listeners
  98. # Set initial state
  99. @client.patch_state({status: 'ON', level: 0}, @id_params)
  100. last_seen = 0
  101. update_timestamp_gaps = []
  102. num_updates = 20
  103. @mqtt_client.on_state(@id_params) do |id, message|
  104. next_time = Time.now
  105. if last_seen != 0
  106. update_timestamp_gaps << next_time - last_seen
  107. end
  108. last_seen = next_time
  109. message['level'] == num_updates
  110. end
  111. (1..num_updates).each do |i|
  112. @mqtt_client.patch_state(@id_params, level: i)
  113. sleep 0.1
  114. end
  115. @mqtt_client.wait_for_listeners
  116. # Discard first, retained messages mess with it
  117. avg = update_timestamp_gaps.sum / update_timestamp_gaps.length
  118. expect(update_timestamp_gaps.length).to be >= 3
  119. expect(avg).to be >= 0.5
  120. end
  121. end
  122. end