mqtt_spec.rb 4.4 KB

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