mqtt_spec.rb 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383
  1. require 'api_client'
  2. RSpec.describe 'MQTT' 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. mqtt_params = mqtt_parameters()
  7. @updates_topic = mqtt_params[:updates_topic]
  8. @topic_prefix = mqtt_topic_prefix()
  9. @client.put(
  10. '/settings',
  11. mqtt_params
  12. )
  13. end
  14. before(:each) do
  15. @id_params = {
  16. id: @client.generate_id,
  17. type: 'rgb_cct',
  18. group_id: 1
  19. }
  20. @client.delete_state(@id_params)
  21. @mqtt_client = create_mqtt_client()
  22. end
  23. context 'deleting' do
  24. it 'should remove retained state' do
  25. @client.patch_state(@id_params, status: 'ON')
  26. seen_blank = false
  27. @mqtt_client.on_state(@id_params) do |topic, message|
  28. seen_blank = (message == "")
  29. end
  30. @client.delete_state(@id_params)
  31. @mqtt_client.wait_for_listeners
  32. expect(seen_blank).to eq(true)
  33. end
  34. end
  35. context 'client status topic' do
  36. before(:all) do
  37. @status_topic = "#{@topic_prefix}client_status"
  38. @client.patch_settings(mqtt_client_status_topic: @status_topic)
  39. end
  40. it 'should send client status messages when configured' do
  41. # Clear any retained messages
  42. @mqtt_client.publish(@status_topic, nil)
  43. # Unfortunately, no way to easily simulate an unclean disconnect, so only test birth
  44. # and forced disconnect
  45. seen_statuses = Set.new
  46. required_statuses = %w(connected disconnected_clean)
  47. @mqtt_client.on_message(@status_topic, 20) do |topic, message|
  48. message = JSON.parse(message)
  49. seen_statuses << message['status']
  50. required_statuses.all? { |x| seen_statuses.include?(x) }
  51. end
  52. # Force MQTT reconnect by updating settings
  53. @client.put('/settings', fakekey: 'fakevalue')
  54. @mqtt_client.wait_for_listeners
  55. expect(seen_statuses).to include(*required_statuses)
  56. end
  57. it 'should send simple client status message when configured' do
  58. @client.patch_settings(simple_mqtt_client_status: true)
  59. # Clear any retained messages
  60. @mqtt_client.publish(@status_topic, nil)
  61. # Unfortunately, no way to easily simulate an unclean disconnect, so only test birth
  62. # and forced disconnect
  63. seen_statuses = Set.new
  64. required_statuses = %w(connected disconnected)
  65. @mqtt_client.on_message(@status_topic, 20) do |topic, message|
  66. seen_statuses << message
  67. required_statuses.all? { |x| seen_statuses.include?(x) }
  68. end
  69. # Force MQTT reconnect by updating settings
  70. @client.patch_settings(fakekey: 'fakevalue')
  71. @mqtt_client.wait_for_listeners
  72. expect(seen_statuses).to include(*required_statuses)
  73. end
  74. end
  75. context 'commands and state' do
  76. # Check state using HTTP
  77. it 'should affect state' do
  78. @client.patch_state({level: 50, status: 'off'}, @id_params)
  79. @mqtt_client.patch_state(@id_params, status: 'on', level: 70)
  80. # wait for packet to be sent...
  81. sleep(1)
  82. state = @client.get_state(@id_params)
  83. expect(state.keys).to include(*%w(level status))
  84. expect(state['status']).to eq('ON')
  85. expect(state['level']).to eq(70)
  86. end
  87. it 'should publish to state topics' do
  88. desired_state = {'status' => 'ON', 'level' => 80}
  89. seen_state = false
  90. @client.patch_state({status: 'off'}, @id_params)
  91. @mqtt_client.on_state(@id_params) do |id, message|
  92. seen_state = (id == @id_params && desired_state.all? { |k,v| v == message[k] })
  93. end
  94. @mqtt_client.patch_state(@id_params, desired_state)
  95. @mqtt_client.wait_for_listeners
  96. expect(seen_state).to be(true)
  97. end
  98. it 'should publish an update message for each new command' do
  99. tweak_params = {'hue' => 49, 'brightness' => 128, 'saturation' => 50}
  100. desired_state = {'state' => 'ON'}.merge(tweak_params)
  101. init_state = desired_state.merge(Hash[
  102. tweak_params.map do |k, v|
  103. [k, v + 10]
  104. end
  105. ])
  106. @client.patch_state(@id_params, init_state)
  107. accumulated_state = {}
  108. @mqtt_client.on_update(@id_params) do |id, message|
  109. desired_state == accumulated_state.merge!(message)
  110. end
  111. @mqtt_client.patch_state(@id_params, desired_state)
  112. @mqtt_client.wait_for_listeners
  113. expect(accumulated_state).to eq(desired_state)
  114. end
  115. it 'should respect the state update interval' do
  116. # Disable updates to prevent the negative effects of spamming commands
  117. @client.put(
  118. '/settings',
  119. mqtt_update_topic_pattern: '',
  120. mqtt_state_rate_limit: 500,
  121. packet_repeats: 1
  122. )
  123. # Set initial state
  124. @client.patch_state({status: 'ON', level: 0}, @id_params)
  125. last_seen = 0
  126. update_timestamp_gaps = []
  127. num_updates = 50
  128. @mqtt_client.on_state(@id_params) do |id, message|
  129. next_time = Time.now
  130. if last_seen != 0
  131. update_timestamp_gaps << next_time - last_seen
  132. end
  133. last_seen = next_time
  134. message['level'] == num_updates
  135. end
  136. (1..num_updates).each do |i|
  137. @mqtt_client.patch_state(@id_params, level: i)
  138. sleep 0.1
  139. end
  140. @mqtt_client.wait_for_listeners
  141. # Discard first, retained messages mess with it
  142. avg = update_timestamp_gaps.sum / update_timestamp_gaps.length
  143. expect(update_timestamp_gaps.length).to be >= 3
  144. expect((avg - 0.5).abs).to be < 0.02
  145. end
  146. end
  147. context ':device_id token for command topic' do
  148. it 'should support hexadecimal device IDs' do
  149. seen = false
  150. @mqtt_client.on_state(@id_params) do |id, message|
  151. seen = (message['status'] == 'ON')
  152. end
  153. # Will use hex by default
  154. @mqtt_client.patch_state(@id_params, status: 'ON')
  155. @mqtt_client.wait_for_listeners
  156. expect(seen).to eq(true), "Should see update for hex param"
  157. end
  158. it 'should support decimal device IDs' do
  159. seen = false
  160. @mqtt_client.on_state(@id_params) do |id, message|
  161. seen = (message['status'] == 'ON')
  162. end
  163. @mqtt_client.publish(
  164. "#{@topic_prefix}commands/#{@id_params[:id]}/rgb_cct/1",
  165. status: 'ON'
  166. )
  167. @mqtt_client.wait_for_listeners
  168. expect(seen).to eq(true), "Should see update for decimal param"
  169. end
  170. end
  171. context ':hex_device_id for command topic' do
  172. before(:all) do
  173. @client.put(
  174. '/settings',
  175. mqtt_topic_pattern: "#{@topic_prefix}commands/:hex_device_id/:device_type/:group_id",
  176. )
  177. end
  178. after(:all) do
  179. @client.put(
  180. '/settings',
  181. mqtt_topic_pattern: "#{@topic_prefix}commands/:device_id/:device_type/:group_id",
  182. )
  183. end
  184. it 'should respond to commands' do
  185. seen = false
  186. @mqtt_client.on_state(@id_params) do |id, message|
  187. seen = (message['status'] == 'ON')
  188. end
  189. # Will use hex by default
  190. @mqtt_client.patch_state(@id_params, status: 'ON')
  191. @mqtt_client.wait_for_listeners
  192. expect(seen).to eq(true), "Should see update for hex param"
  193. end
  194. end
  195. context ':dec_device_id for command topic' do
  196. before(:all) do
  197. @client.put(
  198. '/settings',
  199. mqtt_topic_pattern: "#{@topic_prefix}commands/:dec_device_id/:device_type/:group_id",
  200. )
  201. end
  202. after(:all) do
  203. @client.put(
  204. '/settings',
  205. mqtt_topic_pattern: "#{@topic_prefix}commands/:device_id/:device_type/:group_id",
  206. )
  207. end
  208. it 'should respond to commands' do
  209. seen = false
  210. @mqtt_client.on_state(@id_params) do |id, message|
  211. seen = (message['status'] == 'ON')
  212. end
  213. # Will use hex by default
  214. @mqtt_client.patch_state(@id_params, status: 'ON')
  215. @mqtt_client.wait_for_listeners
  216. expect(seen).to eq(true), "Should see update for hex param"
  217. end
  218. end
  219. context ':hex_device_id for update/state topics' do
  220. before(:all) do
  221. @client.put(
  222. '/settings',
  223. mqtt_state_topic_pattern: "#{@topic_prefix}state/:hex_device_id/:device_type/:group_id",
  224. mqtt_update_topic_pattern: "#{@topic_prefix}updates/:hex_device_id/:device_type/:group_id"
  225. )
  226. end
  227. after(:all) do
  228. @client.put(
  229. '/settings',
  230. mqtt_state_topic_pattern: "#{@topic_prefix}state/:device_id/:device_type/:group_id",
  231. mqtt_update_topic_pattern: "#{@topic_prefix}updates/:device_id/:device_type/:group_id"
  232. )
  233. end
  234. it 'should publish updates with hexadecimal device ID' do
  235. seen_update = false
  236. @mqtt_client.on_update(@id_params) do |id, message|
  237. seen_update = (message['state'] == 'ON')
  238. end
  239. # Will use hex by default
  240. @mqtt_client.patch_state(@id_params, status: 'ON')
  241. @mqtt_client.wait_for_listeners
  242. expect(seen_update).to eq(true)
  243. end
  244. it 'should publish state with hexadecimal device ID' do
  245. seen_state = false
  246. @mqtt_client.on_state(@id_params) do |id, message|
  247. seen_state = (message['status'] == 'ON')
  248. end
  249. # Will use hex by default
  250. @mqtt_client.patch_state(@id_params, status: 'ON')
  251. @mqtt_client.wait_for_listeners
  252. expect(seen_state).to eq(true)
  253. end
  254. end
  255. context ':dec_device_id for update/state topics' do
  256. before(:all) do
  257. @client.put(
  258. '/settings',
  259. mqtt_state_topic_pattern: "#{@topic_prefix}state/:dec_device_id/:device_type/:group_id",
  260. mqtt_update_topic_pattern: "#{@topic_prefix}updates/:dec_device_id/:device_type/:group_id"
  261. )
  262. end
  263. after(:all) do
  264. @client.put(
  265. '/settings',
  266. mqtt_state_topic_pattern: "#{@topic_prefix}state/:device_id/:device_type/:group_id",
  267. mqtt_update_topic_pattern: "#{@topic_prefix}updates/:device_id/:device_type/:group_id"
  268. )
  269. end
  270. it 'should publish updates with hexadecimal device ID' do
  271. seen_update = false
  272. @id_params = @id_params.merge(id_format: 'decimal')
  273. @mqtt_client.on_update(@id_params) do |id, message|
  274. seen_update = (message['state'] == 'ON')
  275. end
  276. # Will use hex by default
  277. @mqtt_client.patch_state(@id_params, status: 'ON')
  278. @mqtt_client.wait_for_listeners
  279. expect(seen_update).to eq(true)
  280. end
  281. it 'should publish state with hexadecimal device ID' do
  282. seen_state = false
  283. @id_params = @id_params.merge(id_format: 'decimal')
  284. @mqtt_client.on_state(@id_params) do |id, message|
  285. seen_state = (message['status'] == 'ON')
  286. end
  287. # Will use hex by default
  288. @mqtt_client.patch_state(@id_params, status: 'ON')
  289. @mqtt_client.wait_for_listeners
  290. expect(seen_state).to eq(true)
  291. end
  292. end
  293. end