Skip to content

Commit

Permalink
Switch sensor readings from MQTT to REST
Browse files Browse the repository at this point in the history
  • Loading branch information
user890104 committed Oct 28, 2023
1 parent edd0787 commit 7519c55
Show file tree
Hide file tree
Showing 11 changed files with 88 additions and 80 deletions.
1 change: 1 addition & 0 deletions .env
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
TITLE=
BACKEND_URL=
DEVICE_BACKEND_URL=
MQTT_BACKEND_URL=
MQTT_URL=
OAUTH_CLIENT_ID=
VARIANT=
1 change: 1 addition & 0 deletions .env.development
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
TITLE=Fauna
BACKEND_URL=http://localhost:3000/
DEVICE_BACKEND_URL=http://localhost:4000/
MQTT_BACKEND_URL=https://mqtt.initlab.org/
MQTT_URL=ws://localhost:1083/mqtt
OAUTH_CLIENT_ID=hCOEcK3ntyBym-uLQkogX6w8457kicVlZbY0PQZJusw
VARIANT=initlab
1 change: 1 addition & 0 deletions .env.production
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
BACKEND_URL=https://fauna.initlab.org/
DEVICE_BACKEND_URL=https://portier.initlab.org/
MQTT_BACKEND_URL=https://mqtt.initlab.org/
MQTT_URL=wss://spitfire.initlab.org:8083/mqtt
OAUTH_CLIENT_ID=YueB6ct6SKPN8Ar72G0LC1QFxW9meUDQIOHdAu5mfCE
14 changes: 9 additions & 5 deletions src/app/store.js
Original file line number Diff line number Diff line change
@@ -1,20 +1,24 @@
import { configureStore } from '@reduxjs/toolkit';
import { setupListeners } from '@reduxjs/toolkit/query';
import { anonymousApiSlice, authenticatedApiSlice, authenticatedDeviceApiSlice } from '../features/apiSlice';
import { sensorSlice } from '../features/sensorSlice';
import {
anonymousApiSlice,
anonymousMqttApiSlice,
authenticatedApiSlice,
authenticatedDeviceApiSlice
} from '../features/apiSlice';
import { doorSlice } from '../features/doorSlice.js';

export const store = configureStore({
reducer: {
[anonymousApiSlice.reducerPath]: anonymousApiSlice.reducer,
[anonymousMqttApiSlice.reducerPath]: anonymousMqttApiSlice.reducer,
[authenticatedApiSlice.reducerPath]: authenticatedApiSlice.reducer,
[authenticatedDeviceApiSlice.reducerPath]: authenticatedDeviceApiSlice.reducer,
[sensorSlice.name]: sensorSlice.reducer,
[doorSlice.name]: doorSlice.reducer,
},
middleware: getDefaultMiddleware =>
getDefaultMiddleware().concat(anonymousApiSlice.middleware).concat(authenticatedApiSlice.middleware)
.concat(authenticatedDeviceApiSlice.middleware),
getDefaultMiddleware().concat(anonymousApiSlice.middleware).concat(anonymousMqttApiSlice.middleware)
.concat(authenticatedApiSlice.middleware).concat(authenticatedDeviceApiSlice.middleware),
});

setupListeners(store.dispatch);
28 changes: 15 additions & 13 deletions src/config.js
Original file line number Diff line number Diff line change
@@ -1,18 +1,5 @@
export const mqtt = {
url: import.meta.env.MQTT_URL,
sensors: [{
type: 'Temperature',
label: 'Big room',
topic: 'sensors-xiaomi-ble/big-room/temperature',
}, {
type: 'Temperature',
label: 'Small room',
topic: 'sensors-xiaomi-ble/small-room/temperature',
}, {
type: 'Temperature',
label: 'Kitchen',
topic: 'sensors-xiaomi-ble/kitchen/temperature',
}],
doorStates: [{
type: 'locked',
topic: 'NetControl/initLab/out/ch28',
Expand All @@ -28,6 +15,21 @@ export const mqtt = {
}],
};

export const sensors = {
'sensors-xiaomi-ble/big-room/temperature': {
type: 'Temperature',
label: 'Big room',
},
'sensors-xiaomi-ble/small-room/temperature': {
type: 'Temperature',
label: 'Small room',
},
'sensors-xiaomi-ble/kitchen/temperature': {
type: 'Temperature',
label: 'Kitchen',
},
};

export const grafana = {
dashboard: {
id: 'SGAb0ZXMk',
Expand Down
17 changes: 17 additions & 0 deletions src/features/apiSlice.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import { refreshTokenIfNeeded } from '../oauth.js';

const apiBaseUrl = import.meta.env.BACKEND_URL + 'api/';
const deviceApiBaseUrl = import.meta.env.DEVICE_BACKEND_URL + 'api/';
const mqttApiBaseUrl = import.meta.env.MQTT_BACKEND_URL;

const anonymousBaseQuery = fetchBaseQuery({
baseUrl: apiBaseUrl,
Expand All @@ -14,6 +15,10 @@ const anonymousBaseQuery = fetchBaseQuery({
},
});

const anonymousMqttBaseQuery = fetchBaseQuery({
baseUrl: mqttApiBaseUrl,
});

const authenticatedBaseQuery = fetchBaseQuery({
baseUrl: apiBaseUrl,
prepareHeaders: headers => {
Expand Down Expand Up @@ -84,6 +89,14 @@ export const anonymousApiSlice = createApi({
}),
});

export const anonymousMqttApiSlice = createApi({
reducerPath: 'anonymousMqttApi',
baseQuery: anonymousMqttBaseQuery,
endpoints: builder => ({
getStatus: query(builder)('status'),
}),
});

export const authenticatedApiSlice = createApi({
reducerPath: 'authenticatedApi',
baseQuery: authenticatedBaseQueryWithReauth,
Expand Down Expand Up @@ -114,6 +127,10 @@ export const {
useGetPresentUsersQuery,
} = anonymousApiSlice;

export const {
useGetStatusQuery,
} = anonymousMqttApiSlice;

export const {
useGetCurrentUserQuery,
} = authenticatedApiSlice;
Expand Down
25 changes: 0 additions & 25 deletions src/features/sensorSlice.js

This file was deleted.

23 changes: 2 additions & 21 deletions src/mqtt.js
Original file line number Diff line number Diff line change
@@ -1,39 +1,20 @@
import { mqtt } from './config';
import { setSensor } from './features/sensorSlice';
import { store } from './app/store';
import { connect } from 'precompiled-mqtt';
import { setState } from './features/doorSlice.js';

const sensorTopics = mqtt.sensors.map(sensor => sensor.topic);
const doorStateTopics = mqtt.doorStates.map(state => state.topic);
const client = connect(mqtt.url);

client.on('connect', function () {
sensorTopics.concat(doorStateTopics).forEach(function (topic) {
doorStateTopics.forEach(function (topic) {
client.subscribe(topic);
});
});

client.on('message', function (topic, data, message) {
client.on('message', function (topic, data) {
data = data.toString();

if (sensorTopics.indexOf(topic) > -1) {
const {
timestamp,
value,
} = JSON.parse(data);

store.dispatch(setSensor({
topic,
timestamp,
value,
message: {
...message,
payload: message.payload.toJSON(),
},
}));
}

const doorState = mqtt.doorStates.filter(state => state.topic === topic).shift();

if (doorState) {
Expand Down
25 changes: 14 additions & 11 deletions src/widgets/SensorReading/SensorReading.jsx
Original file line number Diff line number Diff line change
@@ -1,9 +1,7 @@
import { Card, Col, Container, Row } from 'react-bootstrap';
import PropTypes from 'prop-types';
import './SensorReading.css';
import { useSelector } from 'react-redux';
import { sensorSelector } from '../../features/sensorSlice';
import LoadingIcon from '../icons/LoadingIcon';
import { useDateTimeFormatter } from '../../utils/useDateTimeFormatter.js';

const units = {
Temperature: ['°C', 1],
Expand All @@ -15,15 +13,19 @@ const thresholds = [18, 24, 26, 32];
const SensorReading = ({
type,
label,
topic,
timestamp,
value,
}) => {
const {
timestamp,
value,
} = useSelector(sensorSelector(topic)) || {};
formatDefault,
formatDistanceToNow,
} = useDateTimeFormatter();
const lastUpdate = new Date(timestamp);
const formattedTimestamp = formatDefault(lastUpdate) + ' (' + formatDistanceToNow(lastUpdate) + ')';
const unit = units[type];
const formattedValue = value && value.toFixed(unit[1]) + unit[0];
const formattedValue = value.toFixed(unit[1]) + unit[0];
const isCurrent = timestamp && Date.now() - timestamp <= 3_600_000;
// TODO: only for type === Temperature
const thermometerState = thresholds.filter(threshold => threshold < value).length;

return (<Col>
Expand All @@ -35,8 +37,8 @@ const SensorReading = ({
<i className={'fa-solid fa-5x fa-thermometer-' + thermometerState} />
</Col>
<Col xs={9} className="text-end">
<div className="huge">{formattedValue || <LoadingIcon />}</div>
<div>{label}</div>
<div className="huge">{formattedValue}</div>
<div title={formattedTimestamp}>{label}</div>
</Col>
</Row>
</Container>
Expand All @@ -48,7 +50,8 @@ const SensorReading = ({
SensorReading.propTypes = {
type: PropTypes.string.isRequired,
label: PropTypes.string.isRequired,
topic: PropTypes.string.isRequired,
timestamp: PropTypes.number.isRequired,
value: PropTypes.number.isRequired,
};

export default SensorReading;
32 changes: 27 additions & 5 deletions src/widgets/SensorReadingsWrapper/SensorReadingsWrapper.jsx
Original file line number Diff line number Diff line change
@@ -1,22 +1,44 @@
import { Col, Row } from 'react-bootstrap';
import { mqtt } from '../../config';
import { sensors } from '../../config';
import SensorReading from '../SensorReading/SensorReading';
import { useTranslation } from 'react-i18next';
import { useGetStatusQuery } from '../../features/apiSlice.js';
import LoadingIcon from '../icons/LoadingIcon.jsx';
import ErrorMessage from '../ErrorMessage.jsx';

const SensorReadingsWrapper = () => {
const { t } = useTranslation();
const {
data,
isLoading,
isSuccess,
isError,
error,
} = useGetStatusQuery(undefined, {
pollingInterval: 60_000,
});

return (<>
<Row>
<Col>
<h3>{t('views.dashboard.sensor_readings')}</h3>
</Col>
</Row>
<Row className="row-cols-1 row-cols-lg-3 g-3">
{mqtt.sensors.map((sensor, idx) =>
<SensorReading key={idx} {...sensor} />
{isLoading && <Row>
<Col className="text-center">
<LoadingIcon large />
</Col>
</Row>}
{isError && <Row>
<Col>
<ErrorMessage error={error} />
</Col>
</Row>}
{isSuccess && <Row className="row-cols-1 row-cols-lg-3 g-3">
{Object.entries(sensors).map(([topic, sensor]) =>
<SensorReading key={topic} {...sensor} {...data[topic]} />
)}
</Row>
</Row>}
</>);
};

Expand Down
1 change: 1 addition & 0 deletions vite.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export default defineConfig({
'TITLE',
'BACKEND_URL',
'DEVICE_BACKEND_URL',
'MQTT_BACKEND_URL',
'MQTT_URL',
'OAUTH_CLIENT_ID',
'VARIANT',
Expand Down

0 comments on commit 7519c55

Please sign in to comment.