mqtt_spec.rb 10 KB

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