intercom tab

This commit is contained in:
Svante Kaiser 2023-12-22 21:53:13 +03:00
parent 324d97c0ce
commit 48edd149de
47 changed files with 10995 additions and 10965 deletions

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
data/www/js/179.e21a.js.gz Normal file

Binary file not shown.

Binary file not shown.

View File

@ -11,7 +11,7 @@ import { Layout, RequireAdmin } from './components';
import ProjectRouting from './project/ProjectRouting';
import NetworkConnection from './framework/network/NetworkConnection';
import AccessPoint from './framework/ap/AccessPoint';
import Intercom from './framework/intercom/Intercom';
import NetworkTime from './framework/ntp/NetworkTime';
import Mqtt from './framework/mqtt/Mqtt';
import System from './framework/system/System';
@ -42,7 +42,7 @@ const AuthenticatedRouting: FC = () => {
<Route path={`/${PROJECT_PATH}/*`} element={<ProjectRouting />} />
)}
<Route path="/network/*" element={<NetworkConnection />} />
<Route path="/intercom/*" element={<AccessPoint />} />
<Route path="/intercom/*" element={<Intercom />} />
{features.ntp && (
<Route path="/ntp/*" element={<NetworkTime />} />
)}

View File

@ -1,16 +0,0 @@
import { AxiosPromise } from "axios";
import { APSettings, APStatus } from "../types";
import { AXIOS } from "./endpoints";
export function readAPStatus(): AxiosPromise<APStatus> {
return AXIOS.get('/intercomStatus');
}
export function readAPSettings(): AxiosPromise<APSettings> {
return AXIOS.get('/intercomSettings');
}
export function updateAPSettings(apSettings: APSettings): AxiosPromise<APSettings> {
return AXIOS.post('/intercomSettings', apSettings);
}

View File

@ -0,0 +1,24 @@
import { AxiosPromise } from "axios";
import { IntercomJournal, IntercomSettings, IntercomStatus, SwitchDoorDTO } from "../types";
import { AXIOS } from "./endpoints";
export function readIntercomStatus(): AxiosPromise<IntercomStatus> {
return AXIOS.get('/intercomStatus');
}
export function readIntercomJournal(): AxiosPromise<IntercomJournal> {
return AXIOS.get('/intercomJournal');
}
export function readIntercomSettings(): AxiosPromise<IntercomSettings> {
return AXIOS.get('/intercomSettings');
}
export function switchDoor(switchDoorDTO: SwitchDoorDTO): AxiosPromise<IntercomSettings> {
return AXIOS.post('/switchDoor', switchDoorDTO);
}
export function updateIntercomSettings(intercomSettings: IntercomSettings): AxiosPromise<IntercomSettings> {
return AXIOS.post('/intercomSettings', intercomSettings);
}

View File

@ -1,171 +0,0 @@
import { FC, useState } from 'react';
import { ValidateFieldsError } from 'async-validator';
import { range } from 'lodash';
import { Button, Checkbox, MenuItem } from '@mui/material';
import SaveIcon from '@mui/icons-material/Save';
import * as APApi from "../../api/ap";
import { APProvisionMode, APSettings } from '../../types';
import { BlockFormControlLabel, ButtonRow, FormLoader, SectionContent, ValidatedPasswordField, ValidatedTextField } from '../../components';
import { createAPSettingsValidator, validate } from '../../validators';
import { numberValue, updateValue, useRest } from '../../utils';
export const isAPEnabled = ({ provision_mode }: APSettings) => {
return provision_mode === APProvisionMode.AP_MODE_ALWAYS || provision_mode === APProvisionMode.AP_MODE_DISCONNECTED;
};
const APSettingsForm: FC = () => {
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
const {
loadData, saving, data, setData, saveData, errorMessage
} = useRest<APSettings>({ read: APApi.readAPSettings, update: APApi.updateAPSettings });
const updateFormValue = updateValue(setData);
const content = () => {
if (!data) {
return (<FormLoader onRetry={loadData} errorMessage={errorMessage} />);
}
const validateAndSubmit = async () => {
try {
setFieldErrors(undefined);
await validate(createAPSettingsValidator(data), data);
saveData();
} catch (errors: any) {
setFieldErrors(errors);
}
};
return (
<>
<ValidatedTextField
fieldErrors={fieldErrors}
name="provision_mode"
label="Provide Access Point&hellip;"
value={data.provision_mode}
fullWidth
select
variant="outlined"
onChange={updateFormValue}
margin="normal"
>
<MenuItem value={APProvisionMode.AP_MODE_ALWAYS}>Always</MenuItem>
<MenuItem value={APProvisionMode.AP_MODE_DISCONNECTED}>When WiFi Disconnected</MenuItem>
<MenuItem value={APProvisionMode.AP_NEVER}>Never</MenuItem>
</ValidatedTextField>
{
isAPEnabled(data) &&
<>
<ValidatedTextField
fieldErrors={fieldErrors}
name="ssid"
label="Access Point SSID"
fullWidth
variant="outlined"
value={data.ssid}
onChange={updateFormValue}
margin="normal"
/>
<ValidatedPasswordField
fieldErrors={fieldErrors}
name="password"
label="Access Point Password"
fullWidth
variant="outlined"
value={data.password}
onChange={updateFormValue}
margin="normal"
/>
<ValidatedTextField
fieldErrors={fieldErrors}
name="channel"
label="Preferred Channel"
value={numberValue(data.channel)}
fullWidth
select
type="number"
variant="outlined"
onChange={updateFormValue}
margin="normal"
>
{
range(1, 14).map((i) => <MenuItem key={i} value={i}>{i}</MenuItem>)
}
</ValidatedTextField>
<BlockFormControlLabel
control={
<Checkbox
name="ssid_hidden"
checked={data.ssid_hidden}
onChange={updateFormValue}
/>
}
label="Hide SSID?"
/>
<ValidatedTextField
fieldErrors={fieldErrors}
name="max_clients"
label="Max Clients"
value={numberValue(data.max_clients)}
fullWidth
select
type="number"
variant="outlined"
onChange={updateFormValue}
margin="normal"
>
{
range(1, 9).map((i) => <MenuItem key={i} value={i}>{i}</MenuItem>)
}
</ValidatedTextField>
<ValidatedTextField
fieldErrors={fieldErrors}
name="local_ip"
label="Local IP"
fullWidth
variant="outlined"
value={data.local_ip}
onChange={updateFormValue}
margin="normal"
/>
<ValidatedTextField
fieldErrors={fieldErrors}
name="gateway_ip"
label="Gateway"
fullWidth
variant="outlined"
value={data.gateway_ip}
onChange={updateFormValue}
margin="normal"
/>
<ValidatedTextField
fieldErrors={fieldErrors}
name="subnet_mask"
label="Subnet"
fullWidth
variant="outlined"
value={data.subnet_mask}
onChange={updateFormValue}
margin="normal"
/>
</>
}
<ButtonRow mt={1}>
<Button startIcon={<SaveIcon />} disabled={saving} variant="contained" color="primary" type="submit" onClick={validateAndSubmit}>
Save
</Button>
</ButtonRow>
</>
);
};
return (
<SectionContent title='Access Point Settings' titleGutter>
{content()}
</SectionContent>
);
};
export default APSettingsForm;

View File

@ -1,105 +0,0 @@
import { FC } from "react";
import { Avatar, Button, Divider, List, ListItem, ListItemAvatar, ListItemText, Theme, useTheme } from "@mui/material";
import SettingsInputAntennaIcon from '@mui/icons-material/SettingsInputAntenna';
import DeviceHubIcon from '@mui/icons-material/DeviceHub';
import ComputerIcon from '@mui/icons-material/Computer';
import RefreshIcon from '@mui/icons-material/Refresh';
import * as APApi from "../../api/ap";
import { APNetworkStatus, APStatus } from "../../types";
import { ButtonRow, FormLoader, SectionContent } from "../../components";
import { useRest } from "../../utils";
export const apStatusHighlight = ({ status }: APStatus, theme: Theme) => {
switch (status) {
case APNetworkStatus.ACTIVE:
return theme.palette.success.main;
case APNetworkStatus.INACTIVE:
return theme.palette.info.main;
case APNetworkStatus.LINGERING:
return theme.palette.warning.main;
default:
return theme.palette.warning.main;
}
};
export const apStatus = ({ status }: APStatus) => {
switch (status) {
case APNetworkStatus.ACTIVE:
return "Active";
case APNetworkStatus.INACTIVE:
return "Inactive";
case APNetworkStatus.LINGERING:
return "Lingering until idle";
default:
return "Unknown";
}
};
const APStatusForm: FC = () => {
const { loadData, data, errorMessage } = useRest<APStatus>({ read: APApi.readAPStatus });
const theme = useTheme();
const content = () => {
if (!data) {
return (<FormLoader onRetry={loadData} errorMessage={errorMessage} />);
}
return (
<>
<List>
<ListItem>
<ListItemAvatar>
<Avatar sx={{ bgcolor: apStatusHighlight(data, theme) }}>
<SettingsInputAntennaIcon />
</Avatar>
</ListItemAvatar>
<ListItemText primary="Status" secondary={apStatus(data)} />
</ListItem>
<Divider variant="inset" component="li" />
<ListItem>
<ListItemAvatar>
<Avatar>IP</Avatar>
</ListItemAvatar>
<ListItemText primary="IP Address" secondary={data.ip_address} />
</ListItem>
<Divider variant="inset" component="li" />
<ListItem>
<ListItemAvatar>
<Avatar>
<DeviceHubIcon />
</Avatar>
</ListItemAvatar>
<ListItemText primary="MAC Address" secondary={data.mac_address} />
</ListItem>
<Divider variant="inset" component="li" />
<ListItem>
<ListItemAvatar>
<Avatar>
<ComputerIcon />
</Avatar>
</ListItemAvatar>
<ListItemText primary="AP Clients" secondary={data.station_num} />
</ListItem>
<Divider variant="inset" component="li" />
</List>
<ButtonRow pt={1}>
<Button startIcon={<RefreshIcon />} variant="contained" color="secondary" onClick={loadData}>
Refresh
</Button>
</ButtonRow>
</>
);
};
return (
<SectionContent title='Access Point Status' titleGutter>
{content()}
</SectionContent>
);
};
export default APStatusForm;

View File

@ -6,10 +6,11 @@ import { Tab } from '@mui/material';
import { RequireAdmin, RouterTabs, useLayoutTitle, useRouterTab } from '../../components';
import { AuthenticatedContext } from '../../contexts/authentication';
import APStatusForm from './APStatusForm';
import APSettingsForm from './APSettingsForm';
import IntercomStatusForm from './IntercomStatusForm';
import IntercomJournalForm from './IntercomJournalForm';
import IntercomSettingsForm from './IntercomSettingsForm';
const AccessPoint: FC = () => {
const Intercom: FC = () => {
useLayoutTitle("Access Point");
const authenticatedContext = useContext(AuthenticatedContext);
@ -19,15 +20,17 @@ const AccessPoint: FC = () => {
<>
<RouterTabs value={routerTab}>
<Tab value="status" label="Intercom Status" />
<Tab value="journal" label="Intercom Journal" />
<Tab value="settings" label="Intercom Settings" disabled={!authenticatedContext.me.admin} />
</RouterTabs>
<Routes>
<Route path="status" element={<APStatusForm />} />
<Route path="status" element={<IntercomStatusForm />} />
<Route path="journal" element={<IntercomJournalForm />} />
<Route
path="settings"
element={
<RequireAdmin>
<APSettingsForm />
<IntercomSettingsForm />
</RequireAdmin>
}
/>
@ -38,4 +41,4 @@ const AccessPoint: FC = () => {
};
export default AccessPoint;
export default Intercom;

View File

@ -0,0 +1,63 @@
import { FC } from "react";
import { Button, Divider, List, Paper,
Table, TableBody, TableCell, TableContainer,
TableHead, TableRow, useTheme } from "@mui/material";
import RefreshIcon from '@mui/icons-material/Refresh';
import * as IntercomApi from "../../api/intercom";
import { IntercomJournal } from "../../types";
import { ButtonRow, FormLoader, SectionContent } from "../../components";
import { useRest } from "../../utils";
const IntercomJournalForm: FC = () => {
const { loadData, data, errorMessage } = useRest<IntercomJournal>({ read: IntercomApi.readIntercomJournal });
const theme = useTheme();
const content = () => {
if (!data) {
return (<FormLoader onRetry={loadData} errorMessage={errorMessage} />);
}
return (
<>
<List>
<TableContainer component={Paper}>
<Table>
<TableHead>
<TableRow>
<TableCell>Date</TableCell>
<TableCell>Apartment</TableCell>
</TableRow>
</TableHead>
<TableBody>
{data.notes.map((note, index) => (
<TableRow key={index}>
<TableCell>{note.createdAt}</TableCell>
<TableCell>{note.appartment}</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
<Divider variant="inset" component="li" />
</List>
<ButtonRow pt={1}>
<Button startIcon={<RefreshIcon />} variant="contained" color="secondary" onClick={loadData}>
Refresh
</Button>
</ButtonRow>
</>
);
};
return (
<SectionContent title='Intercom Journal' titleGutter>
{content()}
</SectionContent>
);
};
export default IntercomJournalForm;

View File

@ -0,0 +1,83 @@
import { FC, useState } from 'react';
import { ValidateFieldsError } from 'async-validator';
import { Button, Checkbox, MenuItem } from '@mui/material';
import SaveIcon from '@mui/icons-material/Save';
import * as IntercomApi from "../../api/intercom";
import { IntercomSettings } from '../../types';
import { ButtonRow, FormLoader, SectionContent, ValidatedTextField } from '../../components';
import { createIntercomSettingsValidator, validate } from '../../validators';
import { updateValue, useRest } from '../../utils';
const IntercomSettingsForm: FC = () => {
const [fieldErrors, setFieldErrors] = useState<ValidateFieldsError>();
const {
loadData, saving, data, setData, saveData, errorMessage
} = useRest<IntercomSettings>({ read: IntercomApi.readIntercomSettings, update: IntercomApi.updateIntercomSettings });
const updateFormValue = updateValue(setData);
const content = () => {
if (!data) {
return (<FormLoader onRetry={loadData} errorMessage={errorMessage} />);
}
const validateAndSubmit = async () => {
try {
setFieldErrors(undefined);
await validate(createIntercomSettingsValidator(data), data);
saveData();
} catch (errors: any) {
setFieldErrors(errors);
}
};
return (
<>
<ValidatedTextField
fieldErrors={fieldErrors}
name="kmnModel"
label="KMN Model"
value={data.kmnModel}
fullWidth
select
variant="outlined"
onChange={updateFormValue}
margin="normal"
>
{
data.kmnModelList.map((item) => (
<MenuItem key={item} value={item}>
{item}
</MenuItem>
))
}
</ValidatedTextField>
<ValidatedTextField
fieldErrors={fieldErrors}
name="firstAppartment"
label="First Appartment"
fullWidth
variant="outlined"
value={data.firstAppartment}
onChange={updateFormValue}
margin="normal"
/>
<ButtonRow mt={1}>
<Button startIcon={<SaveIcon />} disabled={saving} variant="contained" color="primary" type="submit" onClick={validateAndSubmit}>
Save
</Button>
</ButtonRow>
</>
);
};
return (
<SectionContent title='Intercom Settings' titleGutter>
{content()}
</SectionContent>
);
};
export default IntercomSettingsForm;

View File

@ -0,0 +1,165 @@
import { FC } from "react";
import { Avatar, Box, Button, Divider, List, ListItem, ListItemAvatar, ListItemText, Theme, useTheme } from "@mui/material";
import PowerIcon from '@mui/icons-material/Power';
import PowerOffIcon from '@mui/icons-material/PowerOff';
import UpdateIcon from '@mui/icons-material/Update';
import DoorFrontIcon from '@mui/icons-material/DoorFront';
import MeetingRoomIcon from '@mui/icons-material/MeetingRoom';
import QuestionMarkIcon from '@mui/icons-material/QuestionMark';
import NotificationsIcon from '@mui/icons-material/Notifications';
import RefreshIcon from '@mui/icons-material/Refresh';
import * as IntercomApi from "../../api/intercom";
import { DoorStatus, IntercomConnectionStatus, IntercomStatus, SwitchDoorType } from "../../types";
import { ButtonRow, FormLoader, SectionContent } from "../../components";
import { useRest } from "../../utils";
import {useSnackbar} from "notistack";
export const intercomStatusHighlight = ({ status }: IntercomStatus, theme: Theme) => {
switch (status) {
case IntercomConnectionStatus.CONNECTED:
return theme.palette.success.main;
case IntercomConnectionStatus.NOT_CONNECTED:
return theme.palette.info.main;
case IntercomConnectionStatus.RECEIVING_DATA:
return theme.palette.success.main;
default:
return theme.palette.warning.main;
}
};
export const intercomStatusIcon = (status: IntercomConnectionStatus) => {
switch (status) {
case IntercomConnectionStatus.CONNECTED:
return <PowerIcon />;
case IntercomConnectionStatus.NOT_CONNECTED:
return <PowerOffIcon />;
case IntercomConnectionStatus.RECEIVING_DATA:
return <UpdateIcon />;
default:
return <QuestionMarkIcon />;
}
};
export const doorStatusIcon = (status: DoorStatus) => {
switch (status) {
case DoorStatus.CLOSED:
return <DoorFrontIcon />;
case DoorStatus.OPENED:
return <MeetingRoomIcon />;
default:
return <QuestionMarkIcon />;
}
};
export const intercomStatus = ({ status }: IntercomStatus) => {
switch (status) {
case IntercomConnectionStatus.CONNECTED:
return "Active";
case IntercomConnectionStatus.NOT_CONNECTED:
return "Inactive";
case IntercomConnectionStatus.RECEIVING_DATA:
return "Receiving data";
default:
return "Unknown";
}
};
export const doorStatus = (status: DoorStatus) => {
switch (status) {
case DoorStatus.OPENED:
return "Opened";
case DoorStatus.CLOSED:
return "Closed";
default:
return "Unknown";
}
};
export const openDoor = () => {
IntercomApi.switchDoor({
type: SwitchDoorType.JOGGING,
status: DoorStatus.OPENED,
time: 1
})
.then(() => {
const { enqueueSnackbar } = useSnackbar();
enqueueSnackbar("Update successful", { variant: 'success' });
})
.catch((error) => {
console.log(error);
});
};
const IntercomStatusForm: FC = () => {
const { loadData, data, errorMessage } = useRest<IntercomStatus>({ read: IntercomApi.readIntercomStatus });
const theme = useTheme();
const content = () => {
if (!data) {
return (<FormLoader onRetry={loadData} errorMessage={errorMessage} />);
}
return (
<>
<List>
<ListItem>
<ListItemAvatar>
<Avatar sx={{ bgcolor: intercomStatusHighlight(data, theme) }}>
{intercomStatusIcon(data.status)}
</Avatar>
</ListItemAvatar>
<ListItemText primary="Status" secondary={intercomStatus(data)} />
</ListItem>
<Divider variant="inset" component="li" />
<ListItem>
<ListItemAvatar>
<Avatar>
<NotificationsIcon />
</Avatar>
</ListItemAvatar>
<ListItemText primary="Last Called Number" secondary={data.lastCalledNumber} />
</ListItem>
<Divider variant="inset" component="li" />
<ListItem>
<ListItemAvatar>
<Avatar>
{doorStatusIcon(data.doorStatus)}
</Avatar>
</ListItemAvatar>
<ListItemText primary="Door Status" secondary={doorStatus(data.doorStatus)} />
</ListItem>
<Divider variant="inset" component="li" />
</List>
<Box display="flex" flexWrap="wrap">
<Box flexGrow={1}>
<ButtonRow mt={1}>
<Button startIcon={<RefreshIcon />} variant="contained" color="secondary" onClick={loadData}>
Refresh
</Button>
</ButtonRow>
</Box>
<Box flexWrap="nowrap" whiteSpace="nowrap">
<ButtonRow mt={1}>
<Button startIcon={<MeetingRoomIcon />} variant="contained" color="info" onClick={openDoor}>
Open door
</Button>
</ButtonRow>
</Box>
</Box>
</>
);
};
return (
<SectionContent title='Intercom Status' titleGutter>
{content()}
</SectionContent>
);
};
export default IntercomStatusForm;

View File

@ -1,18 +1,15 @@
import { FC, useContext, useEffect, useState } from 'react';
import { ValidateFieldsError } from 'async-validator';
import { Avatar, Button, Checkbox, IconButton, List, ListItem, ListItemAvatar, ListItemSecondaryAction, ListItemText } from '@mui/material';
import LockOpenIcon from '@mui/icons-material/LockOpen';
import DeleteIcon from '@mui/icons-material/Delete';
import { Button, Checkbox } from '@mui/material';
import SaveIcon from '@mui/icons-material/Save';
import * as NetworkApi from "../../api/network";
import { NetworkSettings } from '../../types';
import { BlockFormControlLabel, ButtonRow, FormLoader, SectionContent, ValidatedPasswordField, ValidatedTextField } from '../../components';
import { BlockFormControlLabel, ButtonRow, FormLoader, SectionContent, ValidatedTextField } from '../../components';
import { validate, createNetworkSettingsValidator } from '../../validators';
import { updateValue, useRest } from '../../utils';
import { isNetworkOpen, networkSecurityMode } from './NetworkSelector';
import { NetworkConnectionContext } from './NetworkConnectionContext';
const NetworkSettingsForm: FC = () => {

View File

@ -1,30 +0,0 @@
export enum APProvisionMode {
AP_MODE_ALWAYS = 0,
AP_MODE_DISCONNECTED = 1,
AP_NEVER = 2
}
export enum APNetworkStatus {
ACTIVE = 0,
INACTIVE = 1,
LINGERING = 2
}
export interface APStatus {
status: APNetworkStatus;
ip_address: string;
mac_address: string;
station_num: number;
}
export interface APSettings {
provision_mode: APProvisionMode;
ssid: string;
password: string;
channel: number;
ssid_hidden: boolean;
max_clients: number;
local_ip: string;
gateway_ip: string;
subnet_mask: string;
}

View File

@ -1,4 +1,4 @@
export * from './ap';
export * from './intercom';
export * from './features';
export * from './me';
export * from './mqtt';

View File

@ -0,0 +1,43 @@
export enum IntercomConnectionStatus {
CONNECTED = 0,
NOT_CONNECTED = 1,
RECEIVING_DATA = 2
}
export enum DoorStatus {
OPENED = 0,
CLOSED = 1,
}
export enum SwitchDoorType {
ON_OFF = 0,
JOGGING = 1,
DELAY = 2,
}
export interface IntercomStatus {
status: IntercomConnectionStatus;
lastCalledNumber: number;
doorStatus: DoorStatus;
}
export interface IntercomJournalNote {
createdAt: string;
appartment: number;
}
export interface IntercomJournal {
notes: IntercomJournalNote[];
}
export interface IntercomSettings {
kmnModel: string;
kmnModelList: string[];
firstAppartment: number;
}
export interface SwitchDoorDTO {
type: SwitchDoorType;
status: DoorStatus;
time: number;
}

View File

@ -17,6 +17,7 @@ export const extractEventValue = (event: React.ChangeEvent<HTMLInputElement>) =>
export const updateValue = <S>(updateEntity: UpdateEntity<S>) => (
(event: React.ChangeEvent<HTMLInputElement>) => {
console.log(event.target.name);
updateEntity((prevState) => ({
...prevState,
[event.target.name]: extractEventValue(event)

View File

@ -1,4 +1,4 @@
export * from './ap';
export * from './intercom';
export * from './authentication';
export * from './mqtt';
export * from './ntp';

View File

@ -1,13 +1,12 @@
import Schema from 'async-validator';
import { APSettings } from '../types';
import { isAPEnabled } from '../framework/ap/APSettingsForm';
import { IntercomSettings } from '../types';
import { IP_ADDRESS_VALIDATOR } from './shared';
export const createAPSettingsValidator = (apSettings: APSettings) => new Schema({
export const createIntercomSettingsValidator = (intercomSettings: IntercomSettings) => new Schema({
provision_mode: { required: true, message: "Please provide a provision mode" },
...(isAPEnabled(apSettings) && {
...({
ssid: [
{ required: true, message: "Please provide an SSID" },
{ type: "string", max: 32, message: "SSID must be 32 characters or less" }

File diff suppressed because it is too large Load Diff

View File

@ -23,3 +23,4 @@ lib_deps =
ottowinter/AsyncMqttClient-esphome@^0.8.6
ottowinter/ESPAsyncWebServer-esphome@^3.1.0
bblanchon/ArduinoJson@^6.21.3
paulstoffregen/Time@^1.6.1

View File

@ -1,13 +1,15 @@
#include "app/routes.h"
#include "config/config.h"
#include "utils/print.h"
#include "utils/settings.h"
#include "utils/utils.h"
#include "infra/eth.h"
#include "infra/httpServer.h"
#include "infra/mqtt.h"
#include "infra/relay.h"
#include "infra/fs.h"
#include "config/config.h"
#include "utils/print.h"
#include "utils/settings.h"
#include "utils/utils.h"
#include "infra/intercom.h"
#include "domain/intercomJournal.h"
void handleDoorOpen(AsyncWebServerRequest *request) {
relayTurnOn();
@ -37,15 +39,103 @@ void intercomStatus(AsyncWebServerRequest* request) {
AsyncJsonResponse* response = new AsyncJsonResponse(false, MAX_INTERCOM_STATUS_SIZE);
JsonObject root = response->getRoot();
root["status"] = 0;
root["ip_address"] = ETH.localIP().toString();
root["mac_address"] = ETH.macAddress();
root["station_num"] = WiFi.softAPgetStationNum();
root["status"] = (int) getIntercomStatus();
root["lastCalledNumber"] = getLastCalledNumber();
root["doorStatus"] = false;
response->setLength();
request->send(response);
}
static void intercomSettingsRead(AsyncWebServerRequest* request) {
AsyncJsonResponse* response = new AsyncJsonResponse(false, MAX_INTERCOM_SETTINGS_SIZE);
JsonObject root = response->getRoot();
const size_t CAPACITY = JSON_ARRAY_SIZE(3);
StaticJsonDocument<CAPACITY> modelsDoc;
JsonArray models = modelsDoc.to<JsonArray>();
models.add("Vizit");
models.add("Cyfral");
root["kmnModel"] = "Vizit";
root["firstAppartment"] = 1;
root["kmnModelList"] = models;
response->setLength();
request->send(response);
}
static void intercomJournalRead(AsyncWebServerRequest* request) {
AsyncJsonResponse* response = new AsyncJsonResponse(false, MAX_INTERCOM_JOURNAL_SIZE);
JsonObject root = response->getRoot();
const size_t CAPACITY = JSON_ARRAY_SIZE(5*INTERCOM_JOURNAL_FLATS_NUMBER);
StaticJsonDocument<CAPACITY> flatsDoc;
JsonArray flats = flatsDoc.to<JsonArray>();
getIntercomJournalAsJson(flats);
root["notes"] = flats;
response->setLength();
request->send(response);
}
static void intercomSettingsUpdate(AsyncWebServerRequest *request,
uint8_t *data, size_t len, size_t index, size_t total) {
DynamicJsonDocument jsonDoc(MAX_INTERCOM_SETTINGS_SIZE);
String jsonStr = requestDataToStr(data, len);
DeserializationError error = deserializeJson(jsonDoc, jsonStr);
if (!jsonDoc.is<JsonVariant>()) {
request->send(400);
return;
}
JsonVariant root = jsonDoc.as<JsonVariant>();
bool success = writeJsonVariantToFile(INTERCOM_SETTINGS_PATH, root);
if (success) {
String jsonString;
serializeJson(root, jsonString);
request->send(200, "application/json", jsonString.c_str());
}
else
request->send(500, "text/plain", "Intercom settings not updated");
configureIntercom(
root["kmnModel"].as<String>(),
root["firstAppartment"].as<int>()
);
}
static void switchDoor(AsyncWebServerRequest *request,
uint8_t *data, size_t len, size_t index, size_t total) {
DynamicJsonDocument jsonDoc(MAX_INTERCOM_SWITCH_DOOR_SIZE);
String jsonStr = requestDataToStr(data, len);
DeserializationError error = deserializeJson(jsonDoc, jsonStr);
if (!jsonDoc.is<JsonVariant>()) {
request->send(400);
return;
}
JsonVariant root = jsonDoc.as<JsonVariant>();
SwitchDoorType switchDoorType = static_cast<SwitchDoorType>(root["type"].as<int>());
DoorStatus doorStatus = static_cast<DoorStatus>(root["status"].as<int>());
switchRelay(
switchDoorType,
doorStatus,
root["time"]
);
request->send(200, "application/json");
}
void networkStatus(AsyncWebServerRequest* request) {
AsyncJsonResponse* response = new AsyncJsonResponse(false, MAX_NETWORK_STATUS_SIZE);
JsonObject root = response->getRoot();
@ -244,8 +334,10 @@ void initRoutes() {
server.on("/api/v1/networkSettings", HTTP_POST, [](AsyncWebServerRequest *request) {}, NULL, networkSettingsUpdate);
server.on("/api/v1/intercomStatus", intercomStatus);
//server.on("/api/v1/intercomSettings", HTTP_GET, intercomSettingsRead);
//server.on("/api/v1/intercomSettings", HTTP_POST, [](AsyncWebServerRequest *request) {}, NULL, intercomSettingsUpdate);
server.on("/api/v1/intercomJournal", intercomJournalRead);
server.on("/api/v1/intercomSettings", HTTP_GET, intercomSettingsRead);
server.on("/api/v1/intercomSettings", HTTP_POST, [](AsyncWebServerRequest *request) {}, NULL, intercomSettingsUpdate);
server.on("/api/v1/switchDoor", HTTP_POST, [](AsyncWebServerRequest *request) {}, NULL, switchDoor);
server.on("/api/v1/mqttStatus", mqttStatus);
server.on("/api/v1/mqttSettings", HTTP_GET, mqttSettingsRead);

View File

@ -26,6 +26,8 @@
#define FLAT_NUMBER_MQTT_TOPIC "flat_number"
#define STATE_MQTT_TOPIC "state"
#define INTERCOM_JOURNAL_FLATS_NUMBER 100
#define LED_PIN 32
#define DRY_CONT_PIN 15
#define DOOR_SENS_PIN 114
@ -40,10 +42,14 @@
#define MAX_NETWORK_STATUS_SIZE 1024
#define MAX_NETWORK_SETTINGS_SIZE 1024
#define MAX_INTERCOM_STATUS_SIZE 1024
#define MAX_INTERCOM_JOURNAL_SIZE 1024
#define MAX_INTERCOM_SETTINGS_SIZE 1024
#define MAX_INTERCOM_SWITCH_DOOR_SIZE 1024
#define MAX_MQTT_STATUS_SIZE 1024
#define MAX_MQTT_SETTINGS_SIZE 1024
#define MAX_ESP_STATUS_SIZE 1024
#define FS_CONFIG_DIRECTORY "/config"
#define NETWORK_SETTINGS_PATH "/config/networkSettings.json"
#define INTERCOM_SETTINGS_PATH "/config/intercomSettings.json"
#define MQTT_SETTINGS_PATH "/config/mqttSettings.json"

View File

@ -0,0 +1,31 @@
#include "domain/intercomJournal.h"
#include "utils/print.h"
IntercomJournal _intercomJournal;
void addFlatToIntercomJournal(unsigned long createdAt, int appartment) {
for (int i = INTERCOM_JOURNAL_FLATS_NUMBER - 1; i > 0; --i) {
_intercomJournal.notes[i] = _intercomJournal.notes[i - 1];
}
_intercomJournal.notes[0].createdAt = createdAt;
_intercomJournal.notes[0].appartment = appartment;
}
JsonArray getIntercomJournalAsJson(JsonArray& flats) {
for (int i = 0; i < INTERCOM_JOURNAL_FLATS_NUMBER; ++i) {
unsigned long createdAt = _intercomJournal.notes[i].createdAt;
int& appartment = _intercomJournal.notes[i].appartment;
if (createdAt && appartment) {
unsigned long timeDifference = timeModule.getTimeDifference(createdAt);
String formatedTimeDifference = "\n(" + timeModule.getFormattedTimeAgo(timeDifference) + ")";
JsonObject flat = flats.createNestedObject();
flat["createdAt"] = timeModule.getFormattedTime(createdAt) + formatedTimeDifference;
flat["appartment"] = appartment;
}
}
return flats;
}

View File

@ -0,0 +1,19 @@
#include <Arduino.h>
#include <ArduinoJson.h>
#include "config/config.h"
#include "utils/time.h"
extern Time& timeModule;
struct Flat {
unsigned long createdAt;
int appartment;
};
struct IntercomJournal {
Flat notes[INTERCOM_JOURNAL_FLATS_NUMBER];
};
void addFlatToIntercomJournal(unsigned long createdAt, int appartment);
JsonArray getIntercomJournalAsJson(JsonArray& flats);

View File

@ -1,179 +0,0 @@
#include "utils/print.h"
#include "config/config.h"
#include "infra/mqtt.h"
#include "infra/led.h"
#include "domain/stateMachine.h"
State currentState = NOT_CONNECTED;
int countZeros = 0;
int countOnes = 0;
int previousData = 0;
int dataLength = 0;
int signalDuration = 0;
int flat = 0;
void resetCounters() {
countZeros = 0;
countOnes = 0;
previousData = 0;
dataLength = 1;
signalDuration = 0;
}
void writeState(char* message) {
println(message);
publishToMQTT(STATE_MQTT_TOPIC, message);
}
void receiveDigit(int data) {
if (data != previousData) {
if (previousData == HIGH) {
//println("AAAA ", dataLength, " ", signalDuration);
dataLength++;
}
signalDuration = 0;
} else {
signalDuration++;
}
previousData = data;
}
void changeState(State state, bool resetCountersFlag=true) {
currentState = state;
switch (state) {
case NOT_CONNECTED:
ledTurnOff();
writeState("not connected");
break;
case CONNECTED:
ledTurnOn();
writeState("connected");
break;
case RECEIVING_FIRST_DIGIT:
writeState("receiving data");
break;
}
if (resetCountersFlag)
resetCounters();
}
void firstDigitReceived() {
println("| 1 data length: ", dataLength);
flat = dataLength*10;
}
void secondDigitReceived() {
if (dataLength == 11) {
dataLength = 1;
}
flat += dataLength;
flat -= 1;
if (flat > 100 && flat < 110) {
flat -= 100;
}
println("| 2 data length: ", dataLength);
println("| flat: ", flat);
publishToMQTT(FLAT_NUMBER_MQTT_TOPIC, flat);
}
void updateStateMachine(int data) {
switch (currentState) {
case NOT_CONNECTED:
if (data == LOW) {}
else if (data == HIGH) {
changeState(CONNECTED);
}
break;
case CONNECTED:
if (data == LOW) {
countZeros++;
if (countZeros >= NOT_CONNECTED_THRESHOLD) {
changeState(NOT_CONNECTED);
}
} else if (data == HIGH) {
if (countZeros >= INITIALIZING_CALL_THRESHOLD) {
changeState(RECEIVING_FIRST_DIGIT);
}
}
break;
case RECEIVING_FIRST_DIGIT:
receiveDigit(data);
if (data == LOW) {
countOnes = 0;
countZeros++;
} else if (data == HIGH) {
countZeros = 0;
countOnes++;
if (countOnes >= DATA_RECEIVED_THESHOLD) {
firstDigitReceived();
changeState(RECEIVING_SECOND_DIGIT);
}
if (countOnes >= CONNECTED_THRESHOLD) {
changeState(CONNECTED);
}
}
break;
case RECEIVING_SECOND_DIGIT:
receiveDigit(data);
if (data == LOW) {
countOnes = 0;
countZeros++;
} else if (data == HIGH) {
countZeros = 0;
countOnes++;
if (countOnes >= DATA_RECEIVED_THESHOLD) {
secondDigitReceived();
changeState(DATA_RECEIVED);
}
if (countOnes >= CONNECTED_THRESHOLD) {
changeState(CONNECTED);
}
}
break;
case DATA_RECEIVED:
if (data == LOW) {
countZeros++;
if (countZeros >= CALL_ENDED_THRESHOLD) {
changeState(CALL_ENDED);
}
} else if (data == HIGH) {
countOnes++;
if (countOnes >= CONNECTED_THRESHOLD) {
changeState(CONNECTED);
}
break;
}
case CALL_ENDED:
if (data == LOW) {
countZeros++;
if (countZeros >= NOT_CONNECTED_THRESHOLD) {
changeState(NOT_CONNECTED);
}
} else if (data == HIGH) {
countOnes++;
if (countOnes >= CONNECTED_THRESHOLD) {
changeState(CONNECTED);
}
break;
}
}
}
void initStateMachine() {
changeState(CONNECTED);
}

View File

@ -1,25 +0,0 @@
#ifndef STATE_MACHINE_H
#define STATE_MACHINE_H
#include <Arduino.h>
#define CONNECTED_THRESHOLD 50000
#define NOT_CONNECTED_THRESHOLD 50000
#define INITIALIZING_CALL_THRESHOLD 45
#define DATA_RECEIVED_THESHOLD 5000
#define CALL_ENDED_THRESHOLD 10000
enum State {
NOT_CONNECTED,
CONNECTED,
RECEIVING_FIRST_DIGIT,
RECEIVING_SECOND_DIGIT,
DATA_RECEIVED,
CALL_ENDED
};
void resetCounters();
void updateStateMachine(int data);
void initStateMachine();
#endif // STATE_MACHINE_H

View File

@ -1,148 +0,0 @@
#include "utils/print.h"
#include "config/config.h"
#include "infra/mqtt.h"
#include "infra/led.h"
#include "domain/stateMachine.h"
State currentState = NOT_CONNECTED;
int countZeros = 0;
int countOnes = 0;
int previousData = 0;
int dataLength = 0;
int signalDuration = 0;
int flat = 0;
void resetCounters() {
countZeros = 0;
countOnes = 0;
previousData = 0;
dataLength = 0;
signalDuration = 0;
}
void writeState(char* message) {
println(message);
publishToMQTT(STATE_MQTT_TOPIC, message);
}
void receiveData(int data) {
if (data != previousData) {
if (previousData == HIGH) {
dataLength++;
}
signalDuration = 0;
} else {
signalDuration++;
}
previousData = data;
}
void changeState(State state, bool resetCountersFlag=true) {
currentState = state;
switch (state) {
case NOT_CONNECTED:
ledTurnOff();
writeState("not connected");
break;
case CONNECTED:
ledTurnOn();
writeState("connected");
break;
case RECEIVING_DATA:
writeState("receiving data");
break;
}
if (resetCountersFlag)
resetCounters();
}
void flatReceived() {
int flat = dataLength/2;
if (flat < 1)
return;
println("| data length: ", dataLength);
println("| flat: ", flat);
publishToMQTT(FLAT_NUMBER_MQTT_TOPIC, flat);
}
void updateStateMachine(int data) {
switch (currentState) {
case NOT_CONNECTED:
if (data == LOW) {}
else if (data == HIGH) {
changeState(CONNECTED);
}
break;
case CONNECTED:
if (data == LOW) {
countZeros++;
if (countZeros >= NOT_CONNECTED_THRESHOLD) {
changeState(NOT_CONNECTED);
}
} else if (data == HIGH) {
if (countZeros >= INITIALIZING_CALL_THRESHOLD) {
changeState(RECEIVING_DATA);
}
}
break;
case RECEIVING_DATA:
receiveData(data);
if (data == LOW) {
countOnes = 0;
countZeros++;
if (countZeros >= DATA_RECEIVED_THESHOLD) {
flatReceived();
changeState(DATA_RECEIVED);
}
} else if (data == HIGH) {
countZeros = 0;
countOnes++;
if (countOnes >= CONNECTED_THRESHOLD) {
changeState(CONNECTED);
}
}
break;
case DATA_RECEIVED:
if (data == LOW) {
countZeros++;
if (countZeros >= CALL_ENDED_THRESHOLD) {
changeState(CALL_ENDED);
}
} else if (data == HIGH) {
countOnes++;
if (countOnes >= CONNECTED_THRESHOLD) {
changeState(CONNECTED);
}
break;
}
case CALL_ENDED:
if (data == LOW) {
countZeros++;
if (countZeros >= NOT_CONNECTED_THRESHOLD) {
changeState(NOT_CONNECTED);
}
} else if (data == HIGH) {
countOnes++;
if (countOnes >= CONNECTED_THRESHOLD) {
changeState(CONNECTED);
}
break;
}
}
}
void initStateMachine() {
changeState(CONNECTED);
}

View File

@ -1,24 +0,0 @@
#ifndef STATE_MACHINE_H
#define STATE_MACHINE_H
#include <Arduino.h>
#define CONNECTED_THRESHOLD 50000
#define NOT_CONNECTED_THRESHOLD 50000
#define INITIALIZING_CALL_THRESHOLD 15000
#define DATA_RECEIVED_THESHOLD 30000
#define CALL_ENDED_THRESHOLD 10000
enum State {
NOT_CONNECTED,
CONNECTED,
RECEIVING_DATA,
DATA_RECEIVED,
CALL_ENDED
};
void resetCounters();
void updateStateMachine(int data);
void initStateMachine();
#endif // STATE_MACHINE_H

View File

@ -1,5 +1,8 @@
#include "domain/stateMachineController.h"
StateMachineController StateMachineController::instance;
StateMachineController& stateMachineController = StateMachineController::getInstance();
StateMachineController::StateMachineController() : _strategy(nullptr) {}
void StateMachineController::setStrategy(StateMachineStrategy* newStrategy) {
@ -20,8 +23,23 @@ void StateMachineController::updateStateMachine(int data) {
}
StateMachineController::~StateMachineController() {
// Cleanup the strategy in the destructor
if (_strategy) {
delete _strategy;
}
}
StateMachineController& StateMachineController::getInstance() {
return instance;
}
IntercomConnectionStatus StateMachineController::getStatus() {
return _strategy->getStatus();
}
int StateMachineController::getLastCalledNumber() {
return _strategy->getFlat();
}
DoorStatus getDoorStatus() {
return DoorStatus::CLOSED;
}

View File

@ -1,14 +1,24 @@
#pragma once
#include "infra/relay.h"
#include "domain/stateMachineStrategy.h"
class StateMachineController {
private:
static StateMachineController instance;
StateMachineStrategy* _strategy;
public:
StateMachineController();
StateMachineController(const StateMachineController&) = delete;
StateMachineController& operator=(const StateMachineController&) = delete;
~StateMachineController();
public:
void setStrategy(StateMachineStrategy* newStrategy);
void updateStateMachine(int data);
~StateMachineController();
static StateMachineController& getInstance();
IntercomConnectionStatus getStatus();
int getLastCalledNumber();
DoorStatus getDoorStatus();
};

View File

@ -1,8 +1,23 @@
#pragma once
enum class IntercomConnectionStatus { CONNECTED, NOT_CONNECTED, RECEIVING_DATA };
class StateMachineStrategy {
protected:
int _flat = 0;
public:
IntercomConnectionStatus _status = IntercomConnectionStatus::NOT_CONNECTED;
virtual void setup() = 0;
virtual void updateStateMachine(int data) = 0;
virtual ~StateMachineStrategy() {}
int getFlat() {
return _flat;
}
IntercomConnectionStatus getStatus() {
return _status;
}
};

View File

@ -1,4 +1,5 @@
#include "domain/strategies/cyfralStrategy.h"
#include "domain/intercomJournal.h"
CyfralStrategy::CyfralStrategy() {}
@ -111,13 +112,16 @@ void CyfralStrategy::_changeState(State state, bool resetCountersFlag) {
case NOT_CONNECTED:
ledTurnOff();
_writeState("not connected");
_status = IntercomConnectionStatus::NOT_CONNECTED;
break;
case CONNECTED:
ledTurnOn();
_writeState("connected");
_status = IntercomConnectionStatus::CONNECTED;
break;
case RECEIVING_DATA:
_writeState("receiving data");
_status = IntercomConnectionStatus::RECEIVING_DATA;
break;
}
@ -126,14 +130,17 @@ void CyfralStrategy::_changeState(State state, bool resetCountersFlag) {
}
void CyfralStrategy::_flatReceived() {
int flat = _dataLength/2;
_flat = _dataLength/2;
if (flat < 1)
if (_flat < 1)
return;
println("| data length: ", _dataLength);
println("| flat: ", flat);
publishToMQTT(FLAT_NUMBER_MQTT_TOPIC, flat);
println("| flat: ", _flat);
publishToMQTT(FLAT_NUMBER_MQTT_TOPIC, _flat);
unsigned long createdAt = timeModule.getCurrentTime();
addFlatToIntercomJournal(createdAt, _flat);
}
void CyfralStrategy::_initStateMachine() {

View File

@ -1,9 +1,12 @@
#include "utils/print.h"
#include "utils/time.h"
#include "config/config.h"
#include "infra/mqtt.h"
#include "infra/led.h"
#include "domain/stateMachineStrategy.h"
extern Time& timeModule;
class CyfralStrategy : public StateMachineStrategy {
private:
enum State {

View File

@ -1,4 +1,5 @@
#include "domain/strategies/vizitStrategy.h"
#include "domain/intercomJournal.h"
VizitStrategy::VizitStrategy() {}
@ -130,13 +131,16 @@ void VizitStrategy::_changeState(State state, bool resetCountersFlag) {
case NOT_CONNECTED:
ledTurnOff();
_writeState("not connected");
_status = IntercomConnectionStatus::NOT_CONNECTED;
break;
case CONNECTED:
ledTurnOn();
_writeState("connected");
_status = IntercomConnectionStatus::CONNECTED;
break;
case RECEIVING_FIRST_DIGIT:
_writeState("receiving data");
_status = IntercomConnectionStatus::RECEIVING_DATA;
break;
}
@ -164,6 +168,9 @@ void VizitStrategy::_secondDigitReceived() {
println("| 2 data length: ", _dataLength);
println("| flat: ", _flat);
publishToMQTT(FLAT_NUMBER_MQTT_TOPIC, _flat);
unsigned long createdAt = timeModule.getCurrentTime();
addFlatToIntercomJournal(createdAt, _flat);
}
void VizitStrategy::_initStateMachine() {

View File

@ -1,9 +1,12 @@
#include "utils/print.h"
#include "utils/time.h"
#include "config/config.h"
#include "infra/mqtt.h"
#include "infra/led.h"
#include "domain/stateMachineStrategy.h"
extern Time& timeModule;
class VizitStrategy : public StateMachineStrategy {
private:
enum State {
@ -29,8 +32,6 @@ private:
int _dataLength = 0;
int _signalDuration = 0;
int _flat = 0;
void _resetCounters();
void _writeState(char* message);
void _receiveDigit(int data);

View File

@ -0,0 +1,15 @@
#include "infra/intercom.h"
extern StateMachineController& stateMachineController;
void configureIntercom(String kmnModel, int firstAppartment) {
}
IntercomConnectionStatus getIntercomStatus() {
return stateMachineController.getStatus();
}
int getLastCalledNumber() {
return stateMachineController.getLastCalledNumber();
}

View File

@ -0,0 +1,6 @@
#include <Arduino.h>
#include "domain/stateMachineController.h"
void configureIntercom(String kmnModel, int firstAppartment);
IntercomConnectionStatus getIntercomStatus();
int getLastCalledNumber();

View File

@ -1,4 +1,5 @@
#include "infra/relay.h"
#include "utils/utils.h"
void relayTurnOn() {
digitalWrite(DRY_CONT_PIN, LOW);
@ -6,4 +7,33 @@ void relayTurnOn() {
void relayTurnOff() {
digitalWrite(DRY_CONT_PIN, HIGH);
}
void switchRelay(SwitchDoorType type, DoorStatus status, int time) {
switch (type) {
case SwitchDoorType::ON_OFF:
if (status == DoorStatus::OPENED) {
relayTurnOn();
} else {
relayTurnOff();
}
break;
case SwitchDoorType::JOGGING:
if (status == DoorStatus::OPENED) {
relayTurnOn();
delay(secondsToMilliseconds(time));
relayTurnOff();
} else {
relayTurnOff();
delay(secondsToMilliseconds(time));
relayTurnOn();
}
break;
case SwitchDoorType::DELAY:
delay(secondsToMilliseconds(time));
relayTurnOn();
break;
}
}

View File

@ -2,5 +2,9 @@
#include <Arduino.h>
#include "config/config.h"
enum class SwitchDoorType { ON_OFF, JOGGING, DELAY };
enum class DoorStatus { OPENED, CLOSED };
void relayTurnOn();
void relayTurnOff();
void relayTurnOff();
void switchRelay(SwitchDoorType type, DoorStatus status, int time);

View File

@ -30,7 +30,7 @@ bool flag = false;
int zeros = 0;
int ones = 0;
StateMachineController controller;
extern StateMachineController& stateMachineController;
CyfralStrategy strategy;
void IRAM_ATTR one() {
@ -72,13 +72,13 @@ void setup() {
initRoutes();
initHttpServer();
controller.setStrategy(&strategy);
stateMachineController.setStrategy(&strategy);
}
void loop() {
if (lastMicros < micros()) {
data = digitalRead(DATA_PIN);
controller.updateStateMachine(data);
stateMachineController.updateStateMachine(data);
if (PRINT_RAW_SIGNAL_FLAG)
printf("{}", data);

54
src/utils/time.cpp Normal file
View File

@ -0,0 +1,54 @@
#include <TimeLib.h>
#include "utils/time.h"
Time& timeModule = Time::getInstance();
Time::Time() {
initialTime = 0;
}
Time& Time::getInstance() {
static Time instance;
return instance;
}
void Time::setCurrentTime(unsigned long currentTimeMillis) {
initialTime = currentTimeMillis;
}
unsigned long Time::getCurrentTime() const {
return millis() + initialTime;
}
unsigned long Time::getTimeDifference(unsigned long otherTimeMillis) const {
return millis() - otherTimeMillis;
}
String Time::getFormattedTime(unsigned long timeMillis) const {
time_t currentTime = timeMillis / 1000;
struct tm *timeStruct = gmtime(&currentTime);
char buffer[20];
snprintf(buffer, sizeof(buffer), "%04d-%02d-%02d %02d:%02d:%02d",
timeStruct->tm_year + 1900, timeStruct->tm_mon + 1, timeStruct->tm_mday,
timeStruct->tm_hour, timeStruct->tm_min, timeStruct->tm_sec);
return String(buffer);
}
String Time::getFormattedTimeAgo(unsigned long timeMillis) const {
time_t currentTime = timeMillis / 1000;
struct tm *timeStruct = gmtime(&currentTime);
if (timeStruct->tm_yday >= 0) {
return String(timeStruct->tm_yday) + " days ago";
} else if (timeStruct->tm_hour > 0) {
return String(timeStruct->tm_hour) + " hours ago";
} else if (timeStruct->tm_min > 0) {
return String(timeStruct->tm_min) + " minutes ago";
} else {
return String(timeStruct->tm_sec) + " seconds ago";
}
}

17
src/utils/time.h Normal file
View File

@ -0,0 +1,17 @@
#pragma once
#include <Arduino.h>
class Time {
public:
static Time& getInstance();
void setCurrentTime(unsigned long currentTimeMillis);
unsigned long getCurrentTime() const;
unsigned long getTimeDifference(unsigned long otherTimeMillis) const;
String getFormattedTime(unsigned long timeMillis) const;
String getFormattedTimeAgo(unsigned long timeMillis) const;
private:
Time();
unsigned long initialTime;
};

View File

@ -8,4 +8,8 @@ String requestDataToStr(uint8_t *data, size_t len) {
}
return str;
}
unsigned long secondsToMilliseconds(int seconds) {
return static_cast<unsigned long>(seconds) * 1000;
}

View File

@ -2,4 +2,5 @@
#include <Arduino.h>
String requestDataToStr(uint8_t *data, size_t len);
String requestDataToStr(uint8_t *data, size_t len);
unsigned long secondsToMilliseconds(int seconds);