mqtt_spec.rb 9.6 KB

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