auth and change kmn
This commit is contained in:
parent
48edd149de
commit
86d7343f64
1
.gitignore
vendored
1
.gitignore
vendored
@ -6,3 +6,4 @@
|
|||||||
|
|
||||||
build
|
build
|
||||||
node_modules
|
node_modules
|
||||||
|
data/www
|
Binary file not shown.
Binary file not shown.
@ -56,11 +56,11 @@ const IntercomSettingsForm: FC = () => {
|
|||||||
</ValidatedTextField>
|
</ValidatedTextField>
|
||||||
<ValidatedTextField
|
<ValidatedTextField
|
||||||
fieldErrors={fieldErrors}
|
fieldErrors={fieldErrors}
|
||||||
name="firstAppartment"
|
name="firstApartment"
|
||||||
label="First Appartment"
|
label="First Appartment"
|
||||||
fullWidth
|
fullWidth
|
||||||
variant="outlined"
|
variant="outlined"
|
||||||
value={data.firstAppartment}
|
value={data.firstApartment}
|
||||||
onChange={updateFormValue}
|
onChange={updateFormValue}
|
||||||
margin="normal"
|
margin="normal"
|
||||||
/>
|
/>
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
import { FC } from "react";
|
import { FC, useState } from "react";
|
||||||
|
|
||||||
import { Avatar, Box, Button, Divider, List, ListItem, ListItemAvatar, ListItemText, Theme, useTheme } from "@mui/material";
|
import { Avatar, Box, Button, Divider, List, ListItem, ListItemAvatar, ListItemText, Theme, useTheme } from "@mui/material";
|
||||||
import PowerIcon from '@mui/icons-material/Power';
|
import PowerIcon from '@mui/icons-material/Power';
|
||||||
@ -9,12 +9,12 @@ import MeetingRoomIcon from '@mui/icons-material/MeetingRoom';
|
|||||||
import QuestionMarkIcon from '@mui/icons-material/QuestionMark';
|
import QuestionMarkIcon from '@mui/icons-material/QuestionMark';
|
||||||
import NotificationsIcon from '@mui/icons-material/Notifications';
|
import NotificationsIcon from '@mui/icons-material/Notifications';
|
||||||
import RefreshIcon from '@mui/icons-material/Refresh';
|
import RefreshIcon from '@mui/icons-material/Refresh';
|
||||||
|
import { useSnackbar } from "notistack";
|
||||||
|
|
||||||
import * as IntercomApi from "../../api/intercom";
|
import * as IntercomApi from "../../api/intercom";
|
||||||
import { DoorStatus, IntercomConnectionStatus, IntercomStatus, SwitchDoorType } from "../../types";
|
import { DoorStatus, IntercomConnectionStatus, IntercomStatus, SwitchDoorType } from "../../types";
|
||||||
import { ButtonRow, FormLoader, SectionContent } from "../../components";
|
import { ButtonRow, FormLoader, SectionContent } from "../../components";
|
||||||
import { useRest } from "../../utils";
|
import { extractErrorMessage, useRest } from "../../utils";
|
||||||
import {useSnackbar} from "notistack";
|
|
||||||
|
|
||||||
export const intercomStatusHighlight = ({ status }: IntercomStatus, theme: Theme) => {
|
export const intercomStatusHighlight = ({ status }: IntercomStatus, theme: Theme) => {
|
||||||
switch (status) {
|
switch (status) {
|
||||||
@ -77,26 +77,35 @@ export const doorStatus = (status: DoorStatus) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
export const openDoor = () => {
|
export const openDoorApi = async () => {
|
||||||
IntercomApi.switchDoor({
|
await IntercomApi.switchDoor({
|
||||||
type: SwitchDoorType.JOGGING,
|
type: SwitchDoorType.JOGGING,
|
||||||
status: DoorStatus.OPENED,
|
status: DoorStatus.OPENED,
|
||||||
time: 1
|
time: 1
|
||||||
})
|
})
|
||||||
.then(() => {
|
|
||||||
const { enqueueSnackbar } = useSnackbar();
|
|
||||||
enqueueSnackbar("Update successful", { variant: 'success' });
|
|
||||||
})
|
|
||||||
.catch((error) => {
|
|
||||||
console.log(error);
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const IntercomStatusForm: FC = () => {
|
const IntercomStatusForm: FC = () => {
|
||||||
const { loadData, data, errorMessage } = useRest<IntercomStatus>({ read: IntercomApi.readIntercomStatus });
|
const { loadData, data, errorMessage } = useRest<IntercomStatus>({ read: IntercomApi.readIntercomStatus });
|
||||||
|
const { enqueueSnackbar } = useSnackbar();
|
||||||
|
const [confirmRestart, setConfirmRestart] = useState<boolean>(false);
|
||||||
|
const [processing, setProcessing] = useState<boolean>(false);
|
||||||
|
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
|
||||||
|
const openDoor = async () => {
|
||||||
|
setProcessing(true);
|
||||||
|
try {
|
||||||
|
await openDoorApi();
|
||||||
|
enqueueSnackbar("Door is opened", { variant: 'success' });
|
||||||
|
} catch (error: any) {
|
||||||
|
enqueueSnackbar(extractErrorMessage(error, 'Problem restarting device'), { variant: 'error' });
|
||||||
|
} finally {
|
||||||
|
setConfirmRestart(false);
|
||||||
|
setProcessing(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const content = () => {
|
const content = () => {
|
||||||
if (!data) {
|
if (!data) {
|
||||||
return (<FormLoader onRetry={loadData} errorMessage={errorMessage} />);
|
return (<FormLoader onRetry={loadData} errorMessage={errorMessage} />);
|
||||||
|
@ -33,7 +33,7 @@ export interface IntercomJournal {
|
|||||||
export interface IntercomSettings {
|
export interface IntercomSettings {
|
||||||
kmnModel: string;
|
kmnModel: string;
|
||||||
kmnModelList: string[];
|
kmnModelList: string[];
|
||||||
firstAppartment: number;
|
firstApartment: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SwitchDoorDTO {
|
export interface SwitchDoorDTO {
|
||||||
|
@ -5,35 +5,10 @@ import { IntercomSettings } from '../types';
|
|||||||
import { IP_ADDRESS_VALIDATOR } from './shared';
|
import { IP_ADDRESS_VALIDATOR } from './shared';
|
||||||
|
|
||||||
export const createIntercomSettingsValidator = (intercomSettings: IntercomSettings) => new Schema({
|
export const createIntercomSettingsValidator = (intercomSettings: IntercomSettings) => new Schema({
|
||||||
provision_mode: { required: true, message: "Please provide a provision mode" },
|
kmnModel: [],
|
||||||
...({
|
kmnModelList: [],
|
||||||
ssid: [
|
firstApartment: [
|
||||||
{ required: true, message: "Please provide an SSID" },
|
{ required: true, message: "Please provide a first appartment (for example, 1)" },
|
||||||
{ type: "string", max: 32, message: "SSID must be 32 characters or less" }
|
{ type: "number", message: "First appartment should be a number" }
|
||||||
],
|
|
||||||
password: [
|
|
||||||
{ required: true, message: "Please provide an access point password" },
|
|
||||||
{ type: "string", min: 8, max: 64, message: "Password must be 8-64 characters" }
|
|
||||||
],
|
|
||||||
channel: [
|
|
||||||
{ required: true, message: "Please provide a network channel" },
|
|
||||||
{ type: "number", message: "Channel must be between 1 and 14" }
|
|
||||||
],
|
|
||||||
max_clients: [
|
|
||||||
{ required: true, message: "Please specify a value for max clients" },
|
|
||||||
{ type: "number", min: 1, max: 9, message: "Max clients must be between 1 and 9" }
|
|
||||||
],
|
|
||||||
local_ip: [
|
|
||||||
{ required: true, message: "Local IP address is required" },
|
|
||||||
IP_ADDRESS_VALIDATOR
|
|
||||||
],
|
|
||||||
gateway_ip: [
|
|
||||||
{ required: true, message: "Gateway IP address is required" },
|
|
||||||
IP_ADDRESS_VALIDATOR
|
|
||||||
],
|
|
||||||
subnet_mask: [
|
|
||||||
{ required: true, message: "Subnet mask is required" },
|
|
||||||
IP_ADDRESS_VALIDATOR
|
|
||||||
]
|
]
|
||||||
})
|
|
||||||
});
|
});
|
||||||
|
File diff suppressed because it is too large
Load Diff
@ -24,3 +24,4 @@ lib_deps =
|
|||||||
ottowinter/ESPAsyncWebServer-esphome@^3.1.0
|
ottowinter/ESPAsyncWebServer-esphome@^3.1.0
|
||||||
bblanchon/ArduinoJson@^6.21.3
|
bblanchon/ArduinoJson@^6.21.3
|
||||||
paulstoffregen/Time@^1.6.1
|
paulstoffregen/Time@^1.6.1
|
||||||
|
yutter/ArduinoJWT@^1.0.1
|
||||||
|
@ -1,15 +1,20 @@
|
|||||||
#include "app/routes.h"
|
#include "app/routes.h"
|
||||||
#include "config/config.h"
|
#include "config/config.h"
|
||||||
|
|
||||||
#include "utils/print.h"
|
#include "utils/print.h"
|
||||||
#include "utils/settings.h"
|
#include "utils/settings.h"
|
||||||
#include "utils/utils.h"
|
#include "utils/utils.h"
|
||||||
|
#include "utils/errorCodes.h"
|
||||||
|
|
||||||
#include "infra/eth.h"
|
#include "infra/eth.h"
|
||||||
#include "infra/httpServer.h"
|
#include "infra/httpServer.h"
|
||||||
#include "infra/mqtt.h"
|
#include "infra/mqtt.h"
|
||||||
#include "infra/relay.h"
|
#include "infra/relay.h"
|
||||||
#include "infra/fs.h"
|
#include "infra/fs.h"
|
||||||
#include "infra/intercom.h"
|
#include "infra/intercom.h"
|
||||||
#include "domain/intercomJournal.h"
|
|
||||||
|
#include "domain/services/intercomJournal.h"
|
||||||
|
#include "domain/services/auth.h"
|
||||||
|
|
||||||
void handleDoorOpen(AsyncWebServerRequest *request) {
|
void handleDoorOpen(AsyncWebServerRequest *request) {
|
||||||
relayTurnOn();
|
relayTurnOn();
|
||||||
@ -25,7 +30,7 @@ void getFeatures(AsyncWebServerRequest *request) {
|
|||||||
JsonObject root = response->getRoot();
|
JsonObject root = response->getRoot();
|
||||||
|
|
||||||
root["project"] = false;
|
root["project"] = false;
|
||||||
root["security"] = false;
|
root["security"] = true;
|
||||||
root["mqtt"] = true;
|
root["mqtt"] = true;
|
||||||
root["ntp"] = false;
|
root["ntp"] = false;
|
||||||
root["ota"] = false;
|
root["ota"] = false;
|
||||||
@ -47,25 +52,6 @@ void intercomStatus(AsyncWebServerRequest* request) {
|
|||||||
request->send(response);
|
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) {
|
static void intercomJournalRead(AsyncWebServerRequest* request) {
|
||||||
AsyncJsonResponse* response = new AsyncJsonResponse(false, MAX_INTERCOM_JOURNAL_SIZE);
|
AsyncJsonResponse* response = new AsyncJsonResponse(false, MAX_INTERCOM_JOURNAL_SIZE);
|
||||||
JsonObject root = response->getRoot();
|
JsonObject root = response->getRoot();
|
||||||
@ -82,8 +68,24 @@ static void intercomJournalRead(AsyncWebServerRequest* request) {
|
|||||||
request->send(response);
|
request->send(response);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void intercomSettingsRead(AsyncWebServerRequest* request) {
|
||||||
|
AsyncJsonResponse* response = new AsyncJsonResponse(false, MAX_INTERCOM_SETTINGS_SIZE);
|
||||||
|
DynamicJsonDocument doc(1024);
|
||||||
|
|
||||||
|
bool success = readJsonVariantFromFile(INTERCOM_SETTINGS_PATH, doc);
|
||||||
|
JsonVariant root = doc.as<JsonVariant>();
|
||||||
|
|
||||||
|
if (!success)
|
||||||
|
getDefaultIntercomConf(root);
|
||||||
|
|
||||||
|
String jsonString;
|
||||||
|
serializeJson(root, jsonString);
|
||||||
|
request->send(200, "application/json", jsonString.c_str());
|
||||||
|
}
|
||||||
|
|
||||||
static void intercomSettingsUpdate(AsyncWebServerRequest *request,
|
static void intercomSettingsUpdate(AsyncWebServerRequest *request,
|
||||||
uint8_t *data, size_t len, size_t index, size_t total) {
|
uint8_t *data, size_t len, size_t index, size_t total) {
|
||||||
|
|
||||||
DynamicJsonDocument jsonDoc(MAX_INTERCOM_SETTINGS_SIZE);
|
DynamicJsonDocument jsonDoc(MAX_INTERCOM_SETTINGS_SIZE);
|
||||||
String jsonStr = requestDataToStr(data, len);
|
String jsonStr = requestDataToStr(data, len);
|
||||||
DeserializationError error = deserializeJson(jsonDoc, jsonStr);
|
DeserializationError error = deserializeJson(jsonDoc, jsonStr);
|
||||||
@ -95,7 +97,7 @@ uint8_t *data, size_t len, size_t index, size_t total) {
|
|||||||
|
|
||||||
JsonVariant root = jsonDoc.as<JsonVariant>();
|
JsonVariant root = jsonDoc.as<JsonVariant>();
|
||||||
|
|
||||||
bool success = writeJsonVariantToFile(INTERCOM_SETTINGS_PATH, root);
|
bool success = writeJsonToFile(INTERCOM_SETTINGS_PATH, root);
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
String jsonString;
|
String jsonString;
|
||||||
@ -106,8 +108,8 @@ uint8_t *data, size_t len, size_t index, size_t total) {
|
|||||||
request->send(500, "text/plain", "Intercom settings not updated");
|
request->send(500, "text/plain", "Intercom settings not updated");
|
||||||
|
|
||||||
configureIntercom(
|
configureIntercom(
|
||||||
root["kmnModel"].as<String>(),
|
root["kmnModel"],
|
||||||
root["firstAppartment"].as<int>()
|
root["firstApartment"]
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -173,6 +175,7 @@ static void networkSettingsRead(AsyncWebServerRequest* request) {
|
|||||||
|
|
||||||
static void networkSettingsUpdate(AsyncWebServerRequest *request,
|
static void networkSettingsUpdate(AsyncWebServerRequest *request,
|
||||||
uint8_t *data, size_t len, size_t index, size_t total) {
|
uint8_t *data, size_t len, size_t index, size_t total) {
|
||||||
|
|
||||||
DynamicJsonDocument jsonDoc(MAX_NETWORK_SETTINGS_SIZE);
|
DynamicJsonDocument jsonDoc(MAX_NETWORK_SETTINGS_SIZE);
|
||||||
String jsonStr = requestDataToStr(data, len);
|
String jsonStr = requestDataToStr(data, len);
|
||||||
DeserializationError error = deserializeJson(jsonDoc, jsonStr);
|
DeserializationError error = deserializeJson(jsonDoc, jsonStr);
|
||||||
@ -184,7 +187,7 @@ uint8_t *data, size_t len, size_t index, size_t total) {
|
|||||||
|
|
||||||
JsonVariant root = jsonDoc.as<JsonVariant>();
|
JsonVariant root = jsonDoc.as<JsonVariant>();
|
||||||
|
|
||||||
bool success = writeJsonVariantToFile(NETWORK_SETTINGS_PATH, root);
|
bool success = writeJsonToFile(NETWORK_SETTINGS_PATH, root);
|
||||||
|
|
||||||
if (success) {
|
if (success) {
|
||||||
String jsonString;
|
String jsonString;
|
||||||
@ -235,6 +238,7 @@ static void mqttSettingsRead(AsyncWebServerRequest* request) {
|
|||||||
|
|
||||||
static void mqttSettingsUpdate(AsyncWebServerRequest *request,
|
static void mqttSettingsUpdate(AsyncWebServerRequest *request,
|
||||||
uint8_t *data, size_t len, size_t index, size_t total) {
|
uint8_t *data, size_t len, size_t index, size_t total) {
|
||||||
|
|
||||||
DynamicJsonDocument jsonDoc(MAX_MQTT_SETTINGS_SIZE);
|
DynamicJsonDocument jsonDoc(MAX_MQTT_SETTINGS_SIZE);
|
||||||
String jsonStr = requestDataToStr(data, len);
|
String jsonStr = requestDataToStr(data, len);
|
||||||
DeserializationError error = deserializeJson(jsonDoc, jsonStr);
|
DeserializationError error = deserializeJson(jsonDoc, jsonStr);
|
||||||
@ -246,12 +250,9 @@ uint8_t *data, size_t len, size_t index, size_t total) {
|
|||||||
|
|
||||||
JsonVariant root = jsonDoc.as<JsonVariant>();
|
JsonVariant root = jsonDoc.as<JsonVariant>();
|
||||||
|
|
||||||
bool fileLoaded = writeJsonVariantToFile(MQTT_SETTINGS_PATH, root);
|
bool fileLoaded = writeJsonToFile(MQTT_SETTINGS_PATH, root);
|
||||||
bool enabled = root["enabled"].as<bool>();
|
bool enabled = root["enabled"].as<bool>();
|
||||||
|
|
||||||
if (!loadMqttConfig())
|
|
||||||
println("Cannot load MQTT config");
|
|
||||||
|
|
||||||
bool mqttConfigured = configureMqtt(
|
bool mqttConfigured = configureMqtt(
|
||||||
root["enabled"].as<bool>(),
|
root["enabled"].as<bool>(),
|
||||||
root["host"].as<String>(),
|
root["host"].as<String>(),
|
||||||
@ -322,6 +323,53 @@ void factoryReset(AsyncWebServerRequest *request) {
|
|||||||
restartNow(request);
|
restartNow(request);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void verifyAuthorization(AsyncWebServerRequest *request) {
|
||||||
|
request->send(200, "text/plain", "OK");
|
||||||
|
}
|
||||||
|
|
||||||
|
static void signIn(AsyncWebServerRequest *request,
|
||||||
|
uint8_t *data, size_t len, size_t index, size_t total) {
|
||||||
|
|
||||||
|
DynamicJsonDocument jsonDoc(MAX_LOG_IN_SIZE);
|
||||||
|
String jsonStr = requestDataToStr(data, len);
|
||||||
|
println("Access token: ", jsonStr);
|
||||||
|
DeserializationError error = deserializeJson(jsonDoc, jsonStr);
|
||||||
|
|
||||||
|
if (!jsonDoc.is<JsonVariant>() || error) {
|
||||||
|
request->send(400);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
JsonVariant root = jsonDoc.as<JsonVariant>();
|
||||||
|
|
||||||
|
String accessToken;
|
||||||
|
StatusCode status = getAccessToken(
|
||||||
|
root["username"].as<String>(),
|
||||||
|
root["password"].as<String>(),
|
||||||
|
accessToken
|
||||||
|
);
|
||||||
|
println("Access token: ", accessToken);
|
||||||
|
|
||||||
|
if (status == StatusCode::NOT_FOUND) {
|
||||||
|
request->send(403, "text/plain", "Authorization failed");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!accessToken) {
|
||||||
|
request->send(500, "text/plain", "Access token not generated");
|
||||||
|
} else {
|
||||||
|
DynamicJsonDocument jsonDoc(MAX_LOG_IN_SIZE);
|
||||||
|
JsonVariant res = jsonDoc.as<JsonVariant>();
|
||||||
|
|
||||||
|
res["access_token"] = accessToken;
|
||||||
|
|
||||||
|
String jsonString;
|
||||||
|
serializeJson(res, jsonString);
|
||||||
|
println("Sign in response: ", jsonString);
|
||||||
|
|
||||||
|
request->send(200, "application/json", jsonString.c_str());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
void initRoutes() {
|
void initRoutes() {
|
||||||
AsyncWebServer& server = getServer();
|
AsyncWebServer& server = getServer();
|
||||||
server.on("/api/v1/door/open", handleDoorOpen);
|
server.on("/api/v1/door/open", handleDoorOpen);
|
||||||
@ -346,4 +394,7 @@ void initRoutes() {
|
|||||||
server.on("/api/v1/systemStatus", systemStatus);
|
server.on("/api/v1/systemStatus", systemStatus);
|
||||||
server.on("/api/v1/restart", restartNow);
|
server.on("/api/v1/restart", restartNow);
|
||||||
server.on("/api/v1/factoryReset", factoryReset);
|
server.on("/api/v1/factoryReset", factoryReset);
|
||||||
|
|
||||||
|
server.on("/api/v1/verifyAuthorization", HTTP_GET, verifyAuthorization);
|
||||||
|
server.on("/api/v1/signIn", HTTP_POST, [](AsyncWebServerRequest *request) {}, NULL, signIn);
|
||||||
}
|
}
|
@ -16,7 +16,7 @@
|
|||||||
|
|
||||||
#define SERIAL_NUMBER "4823"
|
#define SERIAL_NUMBER "4823"
|
||||||
|
|
||||||
#define DEFAULT_OUTPUT_TOPIC_PATH "/digitum/intercom_bridge4823/out/"
|
#define FACTORY_OUTPUT_TOPIC_PATH "/digitum/intercom_bridge4823/out/"
|
||||||
#define JSON_TOPIC_PATH "/digitum/intercom_bridge4823/out/json"
|
#define JSON_TOPIC_PATH "/digitum/intercom_bridge4823/out/json"
|
||||||
|
|
||||||
#define MAC_ADDRESS_MQTT_TOPIC "mac"
|
#define MAC_ADDRESS_MQTT_TOPIC "mac"
|
||||||
@ -48,8 +48,17 @@
|
|||||||
#define MAX_MQTT_STATUS_SIZE 1024
|
#define MAX_MQTT_STATUS_SIZE 1024
|
||||||
#define MAX_MQTT_SETTINGS_SIZE 1024
|
#define MAX_MQTT_SETTINGS_SIZE 1024
|
||||||
#define MAX_ESP_STATUS_SIZE 1024
|
#define MAX_ESP_STATUS_SIZE 1024
|
||||||
|
#define MAX_LOG_IN_SIZE 1024
|
||||||
|
|
||||||
#define FS_CONFIG_DIRECTORY "/config"
|
#define FS_CONFIG_DIRECTORY "/config"
|
||||||
#define NETWORK_SETTINGS_PATH "/config/networkSettings.json"
|
#define NETWORK_SETTINGS_PATH "/config/networkSettings.json"
|
||||||
#define INTERCOM_SETTINGS_PATH "/config/intercomSettings.json"
|
#define INTERCOM_SETTINGS_PATH "/config/intercomSettings.json"
|
||||||
#define MQTT_SETTINGS_PATH "/config/mqttSettings.json"
|
#define MQTT_SETTINGS_PATH "/config/mqttSettings.json"
|
||||||
|
#define USERS_PATH "/config/users.json"
|
||||||
|
|
||||||
|
#define JWT_SECRET_KEY "secret"
|
||||||
|
#define FACTORY_ADMIN_USERNAME "admin"
|
||||||
|
#define FACTORY_ADMIN_PASSWORD "admin"
|
||||||
|
|
||||||
|
#define FACTORY_KMN_MODEL "Vizit"
|
||||||
|
#define FACTORY_FIRST_APARTMENT 1
|
8
src/domain/entities/user.h
Normal file
8
src/domain/entities/user.h
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <Arduino.h>
|
||||||
|
|
||||||
|
struct User {
|
||||||
|
String username;
|
||||||
|
bool admin;
|
||||||
|
};
|
114
src/domain/repos/userRepo.cpp
Normal file
114
src/domain/repos/userRepo.cpp
Normal file
@ -0,0 +1,114 @@
|
|||||||
|
#include "domain/repos/userRepo.h"
|
||||||
|
#include "config/config.h"
|
||||||
|
#include "infra/fs.h"
|
||||||
|
|
||||||
|
#define MAX_USERS_SIZE 10
|
||||||
|
#define USER_SIZE 1024
|
||||||
|
|
||||||
|
User _jsonVariantToUser(const JsonVariant& json) {
|
||||||
|
User user;
|
||||||
|
|
||||||
|
user.username = json["username"].as<String>();
|
||||||
|
user.admin = json["admin"].as<String>();
|
||||||
|
|
||||||
|
return user;
|
||||||
|
}
|
||||||
|
|
||||||
|
StatusCode createUser(User user) {
|
||||||
|
DynamicJsonDocument doc(USER_SIZE);
|
||||||
|
|
||||||
|
readJsonVariantFromFile(USERS_PATH, doc);
|
||||||
|
|
||||||
|
JsonArray root = doc.as<JsonArray>();
|
||||||
|
|
||||||
|
for (const JsonVariant& userObj: root)
|
||||||
|
if (userObj["username"] == user.username)
|
||||||
|
return StatusCode::CONFLICT;
|
||||||
|
|
||||||
|
DynamicJsonDocument userDoc(USER_SIZE);
|
||||||
|
JsonVariant userVariant = userDoc.as<JsonVariant>();
|
||||||
|
|
||||||
|
userVariant["username"] = user.username;
|
||||||
|
userVariant["admin"] = user.admin;
|
||||||
|
|
||||||
|
root.add(userVariant);
|
||||||
|
|
||||||
|
bool fileLoaded = writeJsonToFile(USERS_PATH, doc.as<JsonVariant>());
|
||||||
|
|
||||||
|
return fileLoaded ? StatusCode::OK : StatusCode::INTERNAL_SERVER_ERROR;
|
||||||
|
}
|
||||||
|
|
||||||
|
User getFactoryUser() {
|
||||||
|
return User{.username = FACTORY_ADMIN_USERNAME, .admin = FACTORY_ADMIN_PASSWORD};
|
||||||
|
}
|
||||||
|
|
||||||
|
User getUser(String username) {
|
||||||
|
DynamicJsonDocument doc(USER_SIZE);
|
||||||
|
|
||||||
|
if (!readJsonVariantFromFile(USERS_PATH, doc)) {
|
||||||
|
User factoryUser = getFactoryUser();
|
||||||
|
if (factoryUser.username == username)
|
||||||
|
return factoryUser;
|
||||||
|
else
|
||||||
|
return User();
|
||||||
|
}
|
||||||
|
|
||||||
|
JsonArray root = doc.as<JsonArray>();
|
||||||
|
|
||||||
|
for (const JsonVariant& userObj: root)
|
||||||
|
if (userObj["username"] == username)
|
||||||
|
return _jsonVariantToUser(userObj);
|
||||||
|
|
||||||
|
return User();
|
||||||
|
}
|
||||||
|
|
||||||
|
String getUsersJson() {
|
||||||
|
DynamicJsonDocument doc(USER_SIZE*MAX_USERS_SIZE);
|
||||||
|
if (!readJsonVariantFromFile(USERS_PATH, doc))
|
||||||
|
return "{}";
|
||||||
|
|
||||||
|
JsonArray root = doc.as<JsonArray>();
|
||||||
|
|
||||||
|
String jsonString;
|
||||||
|
serializeJson(root, jsonString);
|
||||||
|
|
||||||
|
return jsonString;
|
||||||
|
}
|
||||||
|
|
||||||
|
StatusCode deleteUser(String username) {
|
||||||
|
DynamicJsonDocument doc(USER_SIZE);
|
||||||
|
if (!readJsonVariantFromFile(USERS_PATH, doc))
|
||||||
|
return StatusCode::INTERNAL_SERVER_ERROR;
|
||||||
|
|
||||||
|
JsonArray root = doc.as<JsonArray>();
|
||||||
|
|
||||||
|
for (JsonArray::iterator it = root.begin(); it != root.end(); ++it) {
|
||||||
|
if ((*it)["username"] == username) {
|
||||||
|
root.remove(it);
|
||||||
|
|
||||||
|
bool fileLoaded = writeJsonToFile(USERS_PATH, doc.as<JsonVariant>());
|
||||||
|
return fileLoaded ? StatusCode::OK : StatusCode::INTERNAL_SERVER_ERROR;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return StatusCode::NOT_FOUND;
|
||||||
|
}
|
||||||
|
|
||||||
|
StatusCode updateUser(String username, User user) {
|
||||||
|
DynamicJsonDocument doc(USER_SIZE);
|
||||||
|
if (!readJsonVariantFromFile(USERS_PATH, doc))
|
||||||
|
return StatusCode::NOT_FOUND;
|
||||||
|
|
||||||
|
JsonArray root = doc.as<JsonArray>();
|
||||||
|
|
||||||
|
for (const JsonVariant& userObj: root) {
|
||||||
|
if (userObj["username"] == username) {
|
||||||
|
userObj["admin"] = user.admin;
|
||||||
|
|
||||||
|
bool fileLoaded = writeJsonToFile(USERS_PATH, doc.as<JsonVariant>());
|
||||||
|
return fileLoaded ? StatusCode::OK : StatusCode::INTERNAL_SERVER_ERROR;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return StatusCode::NOT_FOUND;
|
||||||
|
}
|
13
src/domain/repos/userRepo.h
Normal file
13
src/domain/repos/userRepo.h
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <Arduino.h>
|
||||||
|
|
||||||
|
#include "utils/errorCodes.h"
|
||||||
|
#include "domain/services/auth.h"
|
||||||
|
#include "domain/entities/user.h"
|
||||||
|
|
||||||
|
StatusCode createUser(User user);
|
||||||
|
User getUser(String username);
|
||||||
|
String getUsersJson();
|
||||||
|
StatusCode deleteUser(String username);
|
||||||
|
StatusCode updateUser(String username, User user);
|
26
src/domain/services/auth.cpp
Normal file
26
src/domain/services/auth.cpp
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
#include "domain/services/auth.h"
|
||||||
|
#include "domain/entities/user.h"
|
||||||
|
#include "domain/repos/userRepo.h"
|
||||||
|
#include "config/config.h"
|
||||||
|
#include "utils/print.h"
|
||||||
|
|
||||||
|
ArduinoJWT jwtService(JWT_SECRET_KEY);
|
||||||
|
|
||||||
|
StatusCode getAccessToken(String username, String password, String& accessToken) {
|
||||||
|
DynamicJsonDocument jsonDoc(MAX_LOG_IN_SIZE);
|
||||||
|
JsonVariant root = jsonDoc.as<JsonVariant>();
|
||||||
|
|
||||||
|
User user = getUser(username);
|
||||||
|
|
||||||
|
if (user.username == "")
|
||||||
|
return StatusCode::NOT_FOUND;
|
||||||
|
|
||||||
|
root["username"] = username;
|
||||||
|
root["admin"] = user.admin;
|
||||||
|
|
||||||
|
String jsonString;
|
||||||
|
serializeJson(root, jsonString);
|
||||||
|
|
||||||
|
accessToken = jwtService.encodeJWT(jsonString);
|
||||||
|
return StatusCode::OK;
|
||||||
|
}
|
9
src/domain/services/auth.h
Normal file
9
src/domain/services/auth.h
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <Arduino.h>
|
||||||
|
#include <ArduinoJson.h>
|
||||||
|
#include <ArduinoJWT.h>
|
||||||
|
|
||||||
|
#include "utils/errorCodes.h"
|
||||||
|
|
||||||
|
StatusCode getAccessToken(String username, String password, String& accessToken);
|
@ -1,4 +1,4 @@
|
|||||||
#include "domain/intercomJournal.h"
|
#include "domain/services/intercomJournal.h"
|
||||||
#include "utils/print.h"
|
#include "utils/print.h"
|
||||||
|
|
||||||
IntercomJournal _intercomJournal;
|
IntercomJournal _intercomJournal;
|
@ -1,3 +1,5 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
#include <Arduino.h>
|
#include <Arduino.h>
|
||||||
#include <ArduinoJson.h>
|
#include <ArduinoJson.h>
|
||||||
|
|
@ -1,4 +1,6 @@
|
|||||||
#include "domain/stateMachineController.h"
|
#include "domain/stateMachineController.h"
|
||||||
|
#include "strategies/cyfralStrategy.h"
|
||||||
|
#include "strategies/vizitStrategy.h"
|
||||||
|
|
||||||
StateMachineController StateMachineController::instance;
|
StateMachineController StateMachineController::instance;
|
||||||
StateMachineController& stateMachineController = StateMachineController::getInstance();
|
StateMachineController& stateMachineController = StateMachineController::getInstance();
|
||||||
@ -43,3 +45,14 @@ int StateMachineController::getLastCalledNumber() {
|
|||||||
DoorStatus getDoorStatus() {
|
DoorStatus getDoorStatus() {
|
||||||
return DoorStatus::CLOSED;
|
return DoorStatus::CLOSED;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void StateMachineController::configure(KMNModel kmnModel, int firstApartment) {
|
||||||
|
switch (kmnModel) {
|
||||||
|
case KMNModel::CYFRAL:
|
||||||
|
this->setStrategy(new CyfralStrategy());
|
||||||
|
break;
|
||||||
|
case KMNModel::VIZIT:
|
||||||
|
this->setStrategy(new VizitStrategy());
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
@ -21,4 +21,6 @@ public:
|
|||||||
IntercomConnectionStatus getStatus();
|
IntercomConnectionStatus getStatus();
|
||||||
int getLastCalledNumber();
|
int getLastCalledNumber();
|
||||||
DoorStatus getDoorStatus();
|
DoorStatus getDoorStatus();
|
||||||
|
|
||||||
|
void configure(KMNModel kmnModel, int firstApartment);
|
||||||
};
|
};
|
@ -1,5 +1,5 @@
|
|||||||
#include "domain/strategies/cyfralStrategy.h"
|
#include "domain/strategies/cyfralStrategy.h"
|
||||||
#include "domain/intercomJournal.h"
|
#include "domain/services/intercomJournal.h"
|
||||||
|
|
||||||
CyfralStrategy::CyfralStrategy() {}
|
CyfralStrategy::CyfralStrategy() {}
|
||||||
|
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
#include "domain/strategies/vizitStrategy.h"
|
#include "domain/strategies/vizitStrategy.h"
|
||||||
#include "domain/intercomJournal.h"
|
#include "domain/services/intercomJournal.h"
|
||||||
|
|
||||||
VizitStrategy::VizitStrategy() {}
|
VizitStrategy::VizitStrategy() {}
|
||||||
|
|
||||||
|
@ -50,7 +50,7 @@ bool createFolderIfNotExists(const char* file_path) {
|
|||||||
return LittleFS.mkdir(folder);
|
return LittleFS.mkdir(folder);
|
||||||
}
|
}
|
||||||
|
|
||||||
bool writeJsonVariantToFile(const char* file_path, JsonVariant& jsonVariant) {
|
bool writeJsonToFile(const char* file_path, const JsonVariant& jsonValue) {
|
||||||
createFolderIfNotExists(file_path);
|
createFolderIfNotExists(file_path);
|
||||||
|
|
||||||
File file = LittleFS.open(file_path, "w");
|
File file = LittleFS.open(file_path, "w");
|
||||||
@ -61,7 +61,7 @@ bool writeJsonVariantToFile(const char* file_path, JsonVariant& jsonVariant) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
String jsonString;
|
String jsonString;
|
||||||
serializeJson(jsonVariant, jsonString);
|
serializeJson(jsonValue, jsonString);
|
||||||
println("Writing config: ", file_path, ", content: ", jsonString);
|
println("Writing config: ", file_path, ", content: ", jsonString);
|
||||||
file.print(jsonString);
|
file.print(jsonString);
|
||||||
|
|
||||||
|
@ -4,5 +4,5 @@
|
|||||||
|
|
||||||
void initFileSystem();
|
void initFileSystem();
|
||||||
bool readJsonVariantFromFile(const char* filename, DynamicJsonDocument& jsonDoc);
|
bool readJsonVariantFromFile(const char* filename, DynamicJsonDocument& jsonDoc);
|
||||||
bool writeJsonVariantToFile(const char* filename, JsonVariant& jsonObj);
|
bool writeJsonToFile(const char* file_path, const JsonVariant& jsonValue);
|
||||||
void deleteFilesInDir(const char* path);
|
void deleteFilesInDir(const char* path);
|
@ -1,9 +1,48 @@
|
|||||||
#include "infra/intercom.h"
|
#include "infra/intercom.h"
|
||||||
|
#include "infra/fs.h"
|
||||||
|
#include "utils/print.h"
|
||||||
|
|
||||||
extern StateMachineController& stateMachineController;
|
extern StateMachineController& stateMachineController;
|
||||||
|
|
||||||
void configureIntercom(String kmnModel, int firstAppartment) {
|
void configureIntercom(String kmnModel, int firstApartment) {
|
||||||
|
KMNModel model;
|
||||||
|
|
||||||
|
if (kmnModel == "Vizit") {
|
||||||
|
model = KMNModel::VIZIT;
|
||||||
|
} else if (kmnModel == "Cyfral") {
|
||||||
|
model = KMNModel::CYFRAL;
|
||||||
|
} else {
|
||||||
|
println("Wrong intercom model: ", kmnModel);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
stateMachineController.configure(model, firstApartment);
|
||||||
|
println("Intercom configured: ", kmnModel, ", first appartment: ", firstApartment);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
bool loadIntercomConfig() {
|
||||||
|
DynamicJsonDocument doc(1024);
|
||||||
|
|
||||||
|
bool success = readJsonVariantFromFile(INTERCOM_SETTINGS_PATH, doc);
|
||||||
|
|
||||||
|
if (!success)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
JsonVariant root = doc.as<JsonVariant>();
|
||||||
|
|
||||||
|
configureIntercom(root["kmnModel"], root["firstApartment"]);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void initIntercom() {
|
||||||
|
if (!loadIntercomConfig() && FACTORY_STATIC_LOCAL_IP) {
|
||||||
|
configureIntercom(
|
||||||
|
FACTORY_KMN_MODEL,
|
||||||
|
FACTORY_FIRST_APARTMENT
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
IntercomConnectionStatus getIntercomStatus() {
|
IntercomConnectionStatus getIntercomStatus() {
|
||||||
@ -13,3 +52,20 @@ IntercomConnectionStatus getIntercomStatus() {
|
|||||||
int getLastCalledNumber() {
|
int getLastCalledNumber() {
|
||||||
return stateMachineController.getLastCalledNumber();
|
return stateMachineController.getLastCalledNumber();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
JsonArray getIntercomModels() {
|
||||||
|
const size_t CAPACITY = JSON_ARRAY_SIZE(3);
|
||||||
|
StaticJsonDocument<CAPACITY> modelsDoc;
|
||||||
|
|
||||||
|
JsonArray models = modelsDoc.to<JsonArray>();
|
||||||
|
models.add("Vizit");
|
||||||
|
models.add("Cyfral");
|
||||||
|
|
||||||
|
return models;
|
||||||
|
}
|
||||||
|
|
||||||
|
void getDefaultIntercomConf(JsonVariant& root) {
|
||||||
|
root["kmnModel"] = FACTORY_KMN_MODEL;
|
||||||
|
root["firstApartment"] = FACTORY_FIRST_APARTMENT;
|
||||||
|
root["kmnModelList"] = getIntercomModels();
|
||||||
|
}
|
@ -1,6 +1,11 @@
|
|||||||
#include <Arduino.h>
|
#include <Arduino.h>
|
||||||
|
#include <ArduinoJson.h>
|
||||||
|
|
||||||
#include "domain/stateMachineController.h"
|
#include "domain/stateMachineController.h"
|
||||||
|
|
||||||
void configureIntercom(String kmnModel, int firstAppartment);
|
void configureIntercom(String kmnModel, int firstApartment);
|
||||||
|
bool loadIntercomConfig();
|
||||||
|
void initIntercom();
|
||||||
IntercomConnectionStatus getIntercomStatus();
|
IntercomConnectionStatus getIntercomStatus();
|
||||||
int getLastCalledNumber();
|
int getLastCalledNumber();
|
||||||
|
void getDefaultIntercomConf(JsonVariant& root);
|
@ -8,10 +8,9 @@ AsyncMqttClient mqttClient;
|
|||||||
TimerHandle_t mqttReconnectTimer;
|
TimerHandle_t mqttReconnectTimer;
|
||||||
AsyncMqttClientDisconnectReason mqttDisconnectReason;
|
AsyncMqttClientDisconnectReason mqttDisconnectReason;
|
||||||
|
|
||||||
DynamicJsonDocument mqttConf(1024);
|
|
||||||
String mqttOutputJson = "";
|
String mqttOutputJson = "";
|
||||||
bool mqttConnected = false;
|
bool mqttConnected = false;
|
||||||
bool mqttEnabled = true;
|
bool mqttEnabled = false;
|
||||||
|
|
||||||
// Pointers to hold retained copies of the mqtt client connection strings.
|
// Pointers to hold retained copies of the mqtt client connection strings.
|
||||||
// This is required as AsyncMqttClient holds refrences to the supplied connection strings.
|
// This is required as AsyncMqttClient holds refrences to the supplied connection strings.
|
||||||
@ -36,7 +35,7 @@ void publishJSONToMQTT(const char* topic, T message) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const char* getFullTopic(const char* topic) {
|
const char* getFullTopic(const char* topic) {
|
||||||
return (String(DEFAULT_OUTPUT_TOPIC_PATH) + String(topic)).c_str();
|
return (String(FACTORY_OUTPUT_TOPIC_PATH) + String(topic)).c_str();
|
||||||
}
|
}
|
||||||
|
|
||||||
void publishToMQTT(const char* topic, const char* message) {
|
void publishToMQTT(const char* topic, const char* message) {
|
||||||
@ -245,16 +244,14 @@ void getDefaultMqttConf(JsonVariant& root) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool loadMqttConfig() {
|
bool loadMqttConfig() {
|
||||||
bool success = readJsonVariantFromFile(MQTT_SETTINGS_PATH, mqttConf);
|
|
||||||
|
|
||||||
if (!success)
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void reconnectMQTTIfNeeded() {
|
void reconnectMQTTIfNeeded() {
|
||||||
if (!mqttClient.connected()) {
|
if (!mqttClient.connected()) {
|
||||||
if (mqttConf.isNull())
|
DynamicJsonDocument mqttConf(1024);
|
||||||
loadMqttConfig();
|
if (!readJsonVariantFromFile(MQTT_SETTINGS_PATH, mqttConf))
|
||||||
|
return;
|
||||||
|
|
||||||
JsonVariant root = mqttConf.as<JsonVariant>();
|
JsonVariant root = mqttConf.as<JsonVariant>();
|
||||||
|
|
||||||
|
@ -21,7 +21,6 @@ AsyncMqttClient& getMqttClient();
|
|||||||
AsyncMqttClientDisconnectReason& getMqttDisconnectReason();
|
AsyncMqttClientDisconnectReason& getMqttDisconnectReason();
|
||||||
bool getMqttConnected();
|
bool getMqttConnected();
|
||||||
bool getMqttEnabled();
|
bool getMqttEnabled();
|
||||||
bool loadMqttConfig();
|
|
||||||
bool configureMqtt(
|
bool configureMqtt(
|
||||||
bool enabled,
|
bool enabled,
|
||||||
String host,
|
String host,
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
|
|
||||||
enum class SwitchDoorType { ON_OFF, JOGGING, DELAY };
|
enum class SwitchDoorType { ON_OFF, JOGGING, DELAY };
|
||||||
enum class DoorStatus { OPENED, CLOSED };
|
enum class DoorStatus { OPENED, CLOSED };
|
||||||
|
enum class KMNModel { CYFRAL, VIZIT };
|
||||||
|
|
||||||
void relayTurnOn();
|
void relayTurnOn();
|
||||||
void relayTurnOff();
|
void relayTurnOff();
|
||||||
|
@ -15,6 +15,7 @@
|
|||||||
#include "infra/relay.h"
|
#include "infra/relay.h"
|
||||||
#include "infra/fs.h"
|
#include "infra/fs.h"
|
||||||
#include "infra/httpServer.h"
|
#include "infra/httpServer.h"
|
||||||
|
#include "infra/intercom.h"
|
||||||
|
|
||||||
#include "app/routes.h"
|
#include "app/routes.h"
|
||||||
|
|
||||||
@ -31,7 +32,6 @@ int zeros = 0;
|
|||||||
int ones = 0;
|
int ones = 0;
|
||||||
|
|
||||||
extern StateMachineController& stateMachineController;
|
extern StateMachineController& stateMachineController;
|
||||||
CyfralStrategy strategy;
|
|
||||||
|
|
||||||
void IRAM_ATTR one() {
|
void IRAM_ATTR one() {
|
||||||
flag = true;
|
flag = true;
|
||||||
@ -71,8 +71,7 @@ void setup() {
|
|||||||
initMQTT();
|
initMQTT();
|
||||||
initRoutes();
|
initRoutes();
|
||||||
initHttpServer();
|
initHttpServer();
|
||||||
|
initIntercom();
|
||||||
stateMachineController.setStrategy(&strategy);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void loop() {
|
void loop() {
|
||||||
|
8
src/utils/errorCodes.h
Normal file
8
src/utils/errorCodes.h
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
enum class StatusCode {
|
||||||
|
OK,
|
||||||
|
NOT_FOUND,
|
||||||
|
CONFLICT,
|
||||||
|
INTERNAL_SERVER_ERROR
|
||||||
|
};
|
@ -42,7 +42,7 @@ String Time::getFormattedTimeAgo(unsigned long timeMillis) const {
|
|||||||
|
|
||||||
struct tm *timeStruct = gmtime(¤tTime);
|
struct tm *timeStruct = gmtime(¤tTime);
|
||||||
|
|
||||||
if (timeStruct->tm_yday >= 0) {
|
if (timeStruct->tm_yday > 0) {
|
||||||
return String(timeStruct->tm_yday) + " days ago";
|
return String(timeStruct->tm_yday) + " days ago";
|
||||||
} else if (timeStruct->tm_hour > 0) {
|
} else if (timeStruct->tm_hour > 0) {
|
||||||
return String(timeStruct->tm_hour) + " hours ago";
|
return String(timeStruct->tm_hour) + " hours ago";
|
||||||
|
Loading…
x
Reference in New Issue
Block a user