mqtt_spec.rb 9.9 KB

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