mqtt.jsx 3.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129
  1. import { h, createContext } from 'preact';
  2. import { baseUrl } from './baseUrl';
  3. import produce from 'immer';
  4. import { useCallback, useContext, useEffect, useRef, useReducer } from 'preact/hooks';
  5. const initialState = Object.freeze({ __connected: false });
  6. export const Mqtt = createContext({ state: initialState, connection: null });
  7. const defaultCreateWebsocket = (url) => new WebSocket(url);
  8. function reducer(state, { topic, payload, retain }) {
  9. switch (topic) {
  10. case '__CLIENT_CONNECTED':
  11. return produce(state, (draftState) => {
  12. draftState.__connected = true;
  13. });
  14. default:
  15. return produce(state, (draftState) => {
  16. let parsedPayload = payload;
  17. try {
  18. parsedPayload = payload && JSON.parse(payload);
  19. } catch (e) {}
  20. draftState[topic] = {
  21. lastUpdate: Date.now(),
  22. payload: parsedPayload,
  23. retain,
  24. };
  25. });
  26. }
  27. }
  28. export function MqttProvider({
  29. config,
  30. children,
  31. createWebsocket = defaultCreateWebsocket,
  32. mqttUrl = `${baseUrl.replace(/^http/, 'ws')}/ws`,
  33. }) {
  34. const [state, dispatch] = useReducer(reducer, initialState);
  35. const wsRef = useRef();
  36. useEffect(() => {
  37. Object.keys(config.cameras).forEach((camera) => {
  38. const { name, clips, detect, snapshots } = config.cameras[camera];
  39. dispatch({ topic: `${name}/clips/state`, payload: clips.enabled ? 'ON' : 'OFF' });
  40. dispatch({ topic: `${name}/detect/state`, payload: detect.enabled ? 'ON' : 'OFF' });
  41. dispatch({ topic: `${name}/snapshots/state`, payload: snapshots.enabled ? 'ON' : 'OFF' });
  42. });
  43. }, [config]);
  44. useEffect(
  45. () => {
  46. const ws = createWebsocket(mqttUrl);
  47. ws.onopen = () => {
  48. dispatch({ topic: '__CLIENT_CONNECTED' });
  49. };
  50. ws.onmessage = (event) => {
  51. dispatch(JSON.parse(event.data));
  52. };
  53. wsRef.current = ws;
  54. return () => {
  55. ws.close(3000, 'Provider destroyed');
  56. };
  57. },
  58. // Forces reconnecting
  59. [state.__reconnectAttempts, mqttUrl] // eslint-disable-line react-hooks/exhaustive-deps
  60. );
  61. return <Mqtt.Provider value={{ state, ws: wsRef.current }}>{children}</Mqtt.Provider>;
  62. }
  63. export function useMqtt(watchTopic, publishTopic) {
  64. const { state, ws } = useContext(Mqtt);
  65. const value = state[watchTopic] || { payload: null };
  66. const send = useCallback(
  67. (payload) => {
  68. ws.send(
  69. JSON.stringify({
  70. topic: publishTopic || watchTopic,
  71. payload: typeof payload !== 'string' ? JSON.stringify(payload) : payload,
  72. })
  73. );
  74. },
  75. [ws, watchTopic, publishTopic]
  76. );
  77. return { value, send, connected: state.__connected };
  78. }
  79. export function useDetectState(camera) {
  80. const {
  81. value: { payload },
  82. send,
  83. connected,
  84. } = useMqtt(`${camera}/detect/state`, `${camera}/detect/set`);
  85. return { payload, send, connected };
  86. }
  87. export function useClipsState(camera) {
  88. const {
  89. value: { payload },
  90. send,
  91. connected,
  92. } = useMqtt(`${camera}/clips/state`, `${camera}/clips/set`);
  93. return { payload, send, connected };
  94. }
  95. export function useSnapshotsState(camera) {
  96. const {
  97. value: { payload },
  98. send,
  99. connected,
  100. } = useMqtt(`${camera}/snapshots/state`, `${camera}/snapshots/set`);
  101. return { payload, send, connected };
  102. }
  103. export function useRestart() {
  104. const {
  105. value: { payload },
  106. send,
  107. connected,
  108. } = useMqtt('restart', 'restart');
  109. return { payload, send, connected };
  110. }