mirror of
https://github.com/emsesp/EMS-ESP32.git
synced 2025-12-07 00:09:51 +03:00
341 lines
9.5 KiB
TypeScript
341 lines
9.5 KiB
TypeScript
import Schema from 'async-validator';
|
|
import type { InternalRuleItem } from 'async-validator';
|
|
import { IP_OR_HOSTNAME_VALIDATOR } from 'validators/shared';
|
|
|
|
import type {
|
|
AnalogSensor,
|
|
DeviceValue,
|
|
EntityItem,
|
|
ScheduleItem,
|
|
Settings,
|
|
TemperatureSensor
|
|
} from './types';
|
|
|
|
// Constants
|
|
const ERROR_MESSAGES = {
|
|
GPIO_INVALID: 'Must be an valid GPIO port',
|
|
NAME_DUPLICATE: 'Name already in use',
|
|
GPIO_DUPLICATE: 'GPIO already in use',
|
|
VALUE_OUT_OF_RANGE: 'Value out of range',
|
|
HEX_REQUIRED: 'Is required and must be in hex format'
|
|
} as const;
|
|
|
|
const VALIDATION_LIMITS = {
|
|
PORT_MIN: 0,
|
|
PORT_MAX: 65535,
|
|
MODBUS_MAX_CLIENTS_MIN: 0,
|
|
MODBUS_MAX_CLIENTS_MAX: 50,
|
|
MODBUS_TIMEOUT_MIN: 100,
|
|
MODBUS_TIMEOUT_MAX: 20000,
|
|
SYSLOG_MARK_INTERVAL_MIN: 0,
|
|
SYSLOG_MARK_INTERVAL_MAX: 3600,
|
|
SHOWER_MIN_DURATION_MIN: 10,
|
|
SHOWER_MIN_DURATION_MAX: 360,
|
|
SHOWER_ALERT_TRIGGER_MIN: 1,
|
|
SHOWER_ALERT_TRIGGER_MAX: 20,
|
|
SHOWER_ALERT_COLDSHOT_MIN: 1,
|
|
SHOWER_ALERT_COLDSHOT_MAX: 10,
|
|
REMOTE_TIMEOUT_MIN: 1,
|
|
REMOTE_TIMEOUT_MAX: 240,
|
|
OFFSET_MIN: 0,
|
|
OFFSET_MAX: 255,
|
|
COMMAND_MIN: 1,
|
|
COMMAND_MAX: 300,
|
|
NAME_MAX_LENGTH: 19,
|
|
HEX_BASE: 16
|
|
} as const;
|
|
|
|
type ValidationRules = Array<{
|
|
required?: boolean;
|
|
message?: string;
|
|
[key: string]: unknown;
|
|
}>;
|
|
|
|
export const createSettingsValidator = (settings: Settings) => {
|
|
const schema: Record<string, ValidationRules> = {};
|
|
|
|
// Syslog validations
|
|
if (settings.syslog_enabled) {
|
|
schema.syslog_host = [
|
|
{ required: true, message: 'Host is required' },
|
|
IP_OR_HOSTNAME_VALIDATOR
|
|
];
|
|
schema.syslog_port = [
|
|
{ required: true, message: 'Port is required' },
|
|
{
|
|
type: 'number',
|
|
min: VALIDATION_LIMITS.PORT_MIN,
|
|
max: VALIDATION_LIMITS.PORT_MAX,
|
|
message: 'Invalid Port'
|
|
}
|
|
];
|
|
schema.syslog_mark_interval = [
|
|
{ required: true, message: 'Mark interval is required' },
|
|
{
|
|
type: 'number',
|
|
min: VALIDATION_LIMITS.SYSLOG_MARK_INTERVAL_MIN,
|
|
max: VALIDATION_LIMITS.SYSLOG_MARK_INTERVAL_MAX,
|
|
message: `Must be between ${VALIDATION_LIMITS.SYSLOG_MARK_INTERVAL_MIN} and ${VALIDATION_LIMITS.SYSLOG_MARK_INTERVAL_MAX}`
|
|
}
|
|
];
|
|
}
|
|
|
|
// Modbus validations
|
|
if (settings.modbus_enabled) {
|
|
schema.modbus_max_clients = [
|
|
{ required: true, message: 'Max clients is required' },
|
|
{
|
|
type: 'number',
|
|
min: VALIDATION_LIMITS.MODBUS_MAX_CLIENTS_MIN,
|
|
max: VALIDATION_LIMITS.MODBUS_MAX_CLIENTS_MAX,
|
|
message: 'Invalid number'
|
|
}
|
|
];
|
|
schema.modbus_port = [
|
|
{ required: true, message: 'Port is required' },
|
|
{
|
|
type: 'number',
|
|
min: VALIDATION_LIMITS.PORT_MIN,
|
|
max: VALIDATION_LIMITS.PORT_MAX,
|
|
message: 'Invalid Port'
|
|
}
|
|
];
|
|
schema.modbus_timeout = [
|
|
{ required: true, message: 'Timeout is required' },
|
|
{
|
|
type: 'number',
|
|
min: VALIDATION_LIMITS.MODBUS_TIMEOUT_MIN,
|
|
max: VALIDATION_LIMITS.MODBUS_TIMEOUT_MAX,
|
|
message: `Must be between ${VALIDATION_LIMITS.MODBUS_TIMEOUT_MIN} and ${VALIDATION_LIMITS.MODBUS_TIMEOUT_MAX}`
|
|
}
|
|
];
|
|
}
|
|
|
|
// Shower timer validations
|
|
if (settings.shower_timer) {
|
|
schema.shower_min_duration = [
|
|
{
|
|
type: 'number',
|
|
min: VALIDATION_LIMITS.SHOWER_MIN_DURATION_MIN,
|
|
max: VALIDATION_LIMITS.SHOWER_MIN_DURATION_MAX,
|
|
message: `Time must be between ${VALIDATION_LIMITS.SHOWER_MIN_DURATION_MIN} and ${VALIDATION_LIMITS.SHOWER_MIN_DURATION_MAX} seconds`
|
|
}
|
|
];
|
|
}
|
|
|
|
// Shower alert validations
|
|
if (settings.shower_alert) {
|
|
schema.shower_alert_trigger = [
|
|
{
|
|
type: 'number',
|
|
min: VALIDATION_LIMITS.SHOWER_ALERT_TRIGGER_MIN,
|
|
max: VALIDATION_LIMITS.SHOWER_ALERT_TRIGGER_MAX,
|
|
message: `Time must be between ${VALIDATION_LIMITS.SHOWER_ALERT_TRIGGER_MIN} and ${VALIDATION_LIMITS.SHOWER_ALERT_TRIGGER_MAX} minutes`
|
|
}
|
|
];
|
|
schema.shower_alert_coldshot = [
|
|
{
|
|
type: 'number',
|
|
min: VALIDATION_LIMITS.SHOWER_ALERT_COLDSHOT_MIN,
|
|
max: VALIDATION_LIMITS.SHOWER_ALERT_COLDSHOT_MAX,
|
|
message: `Time must be between ${VALIDATION_LIMITS.SHOWER_ALERT_COLDSHOT_MIN} and ${VALIDATION_LIMITS.SHOWER_ALERT_COLDSHOT_MAX} seconds`
|
|
}
|
|
];
|
|
}
|
|
|
|
// Remote timeout validations
|
|
if (settings.remote_timeout_en) {
|
|
schema.remote_timeout = [
|
|
{
|
|
type: 'number',
|
|
min: VALIDATION_LIMITS.REMOTE_TIMEOUT_MIN,
|
|
max: VALIDATION_LIMITS.REMOTE_TIMEOUT_MAX,
|
|
message: `Timeout must be between ${VALIDATION_LIMITS.REMOTE_TIMEOUT_MIN} and ${VALIDATION_LIMITS.REMOTE_TIMEOUT_MAX} hours`
|
|
}
|
|
];
|
|
}
|
|
|
|
return new Schema(schema);
|
|
};
|
|
|
|
// Generic unique name validator factory
|
|
const createUniqueNameValidator = <T extends { name: string }>(
|
|
items: T[],
|
|
originalName?: string
|
|
) => ({
|
|
validator(
|
|
_rule: InternalRuleItem,
|
|
name: string,
|
|
callback: (error?: string) => void
|
|
) {
|
|
if (
|
|
name !== '' &&
|
|
(originalName === undefined ||
|
|
originalName.toLowerCase() !== name.toLowerCase()) &&
|
|
items.find((item) => item.name.toLowerCase() === name.toLowerCase())
|
|
) {
|
|
callback(ERROR_MESSAGES.NAME_DUPLICATE);
|
|
return;
|
|
}
|
|
callback();
|
|
}
|
|
});
|
|
|
|
// Generic field name validator (for cases where the name field has different property names)
|
|
const createUniqueFieldNameValidator = <T>(
|
|
items: T[],
|
|
getName: (item: T) => string,
|
|
originalName?: string
|
|
) => ({
|
|
validator(
|
|
_rule: InternalRuleItem,
|
|
name: string,
|
|
callback: (error?: string) => void
|
|
) {
|
|
if (
|
|
name !== '' &&
|
|
(originalName === undefined ||
|
|
originalName.toLowerCase() !== name.toLowerCase()) &&
|
|
items.find((item) => getName(item).toLowerCase() === name.toLowerCase())
|
|
) {
|
|
callback(ERROR_MESSAGES.NAME_DUPLICATE);
|
|
return;
|
|
}
|
|
callback();
|
|
}
|
|
});
|
|
|
|
const NAME_PATTERN_BASE = '[a-zA-Z0-9_]';
|
|
const NAME_PATTERN_MESSAGE = `Must be <${VALIDATION_LIMITS.NAME_MAX_LENGTH + 1} characters: alphanumeric or '_'`;
|
|
|
|
const NAME_PATTERN = {
|
|
type: 'string' as const,
|
|
pattern: new RegExp(
|
|
`^${NAME_PATTERN_BASE}{0,${VALIDATION_LIMITS.NAME_MAX_LENGTH}}$`
|
|
),
|
|
message: NAME_PATTERN_MESSAGE
|
|
};
|
|
|
|
const NAME_PATTERN_REQUIRED = {
|
|
type: 'string' as const,
|
|
pattern: new RegExp(
|
|
`^${NAME_PATTERN_BASE}{1,${VALIDATION_LIMITS.NAME_MAX_LENGTH}}$`
|
|
),
|
|
message: NAME_PATTERN_MESSAGE
|
|
};
|
|
|
|
export const uniqueNameValidator = (schedule: ScheduleItem[], o_name?: string) =>
|
|
createUniqueNameValidator(schedule, o_name);
|
|
|
|
export const schedulerItemValidation = (
|
|
schedule: ScheduleItem[],
|
|
scheduleItem: ScheduleItem
|
|
) =>
|
|
new Schema({
|
|
name: [NAME_PATTERN, uniqueNameValidator(schedule, scheduleItem.o_name)],
|
|
cmd: [
|
|
{ required: true, message: 'Command is required' },
|
|
{
|
|
type: 'string',
|
|
min: VALIDATION_LIMITS.COMMAND_MIN,
|
|
max: VALIDATION_LIMITS.COMMAND_MAX,
|
|
message: `Command must be ${VALIDATION_LIMITS.COMMAND_MIN}-${VALIDATION_LIMITS.COMMAND_MAX} characters`
|
|
}
|
|
]
|
|
});
|
|
|
|
export const uniqueCustomNameValidator = (entity: EntityItem[], o_name?: string) =>
|
|
createUniqueNameValidator(entity, o_name);
|
|
|
|
const hexValidator = {
|
|
validator(
|
|
_rule: InternalRuleItem,
|
|
value: string,
|
|
callback: (error?: string) => void
|
|
) {
|
|
if (!value || Number.isNaN(Number.parseInt(value, VALIDATION_LIMITS.HEX_BASE))) {
|
|
callback(ERROR_MESSAGES.HEX_REQUIRED);
|
|
return;
|
|
}
|
|
callback();
|
|
}
|
|
};
|
|
|
|
export const entityItemValidation = (entity: EntityItem[], entityItem: EntityItem) =>
|
|
new Schema({
|
|
name: [
|
|
{ required: true, message: 'Name is required' },
|
|
NAME_PATTERN_REQUIRED,
|
|
uniqueCustomNameValidator(entity, entityItem.o_name)
|
|
],
|
|
device_id: [hexValidator],
|
|
type_id: [hexValidator],
|
|
offset: [
|
|
{ required: true, message: 'Offset is required' },
|
|
{
|
|
type: 'number',
|
|
min: VALIDATION_LIMITS.OFFSET_MIN,
|
|
max: VALIDATION_LIMITS.OFFSET_MAX,
|
|
message: `Must be between ${VALIDATION_LIMITS.OFFSET_MIN} and ${VALIDATION_LIMITS.OFFSET_MAX}`
|
|
}
|
|
],
|
|
factor: [{ required: true, message: 'is required' }]
|
|
});
|
|
|
|
export const uniqueTemperatureNameValidator = (
|
|
sensors: TemperatureSensor[],
|
|
o_name?: string
|
|
) => createUniqueFieldNameValidator(sensors, (s) => s.n, o_name);
|
|
|
|
export const temperatureSensorItemValidation = (
|
|
sensors: TemperatureSensor[],
|
|
sensor: TemperatureSensor
|
|
) =>
|
|
new Schema({
|
|
n: [NAME_PATTERN, uniqueTemperatureNameValidator(sensors, sensor.o_n)]
|
|
});
|
|
|
|
export const uniqueAnalogNameValidator = (
|
|
sensors: AnalogSensor[],
|
|
o_name?: string
|
|
) => createUniqueFieldNameValidator(sensors, (s) => s.n, o_name);
|
|
|
|
export const analogSensorItemValidation = (
|
|
sensors: AnalogSensor[],
|
|
sensor: AnalogSensor
|
|
) => {
|
|
return new Schema({
|
|
// name is required and must be unique
|
|
n: [
|
|
{ required: true, message: 'Name is required' },
|
|
NAME_PATTERN,
|
|
uniqueAnalogNameValidator(sensors, sensor.o_n)
|
|
]
|
|
});
|
|
};
|
|
|
|
export const deviceValueItemValidation = (dv: DeviceValue) =>
|
|
new Schema({
|
|
v: [
|
|
{ required: true, message: 'Value is required' },
|
|
{
|
|
validator(
|
|
_rule: InternalRuleItem,
|
|
value: unknown,
|
|
callback: (error?: string) => void
|
|
) {
|
|
if (
|
|
typeof value === 'number' &&
|
|
dv.m !== undefined &&
|
|
dv.x !== undefined &&
|
|
(value < dv.m || value > dv.x)
|
|
) {
|
|
callback(ERROR_MESSAGES.VALUE_OUT_OF_RANGE);
|
|
return;
|
|
}
|
|
callback();
|
|
}
|
|
}
|
|
]
|
|
});
|