Files
QSY-lvjuxcx/utils/smart-device.js
2026-06-29 09:42:53 +08:00

810 lines
23 KiB
JavaScript

import { getCurrentLanguage } from './i18n'
function safeJsonParse(value, fallback = null) {
if (typeof value !== 'string') {
return value
}
const text = value.trim()
if (!text) {
return fallback
}
const looksLikeJson = (
(text.startsWith('{') && text.endsWith('}'))
|| (text.startsWith('[') && text.endsWith(']'))
)
if (!looksLikeJson) {
return fallback
}
try {
return JSON.parse(text)
} catch (error) {
return fallback
}
}
function pickFirstFilledValue(...values) {
for (let index = 0; index < values.length; index += 1) {
const value = values[index]
if (value === undefined || value === null) {
continue
}
const text = String(value).trim()
if (text) {
return text
}
}
return ''
}
function getSmartDeviceText(key = '', params = {}) {
const isEn = getCurrentLanguage() === 'en'
const map = {
commonSwitch: isEn ? 'General Switch' : '通用开关',
slideControl: isEn ? 'Slider Control' : '滑动控制',
curtainButton: isEn ? 'Curtain Button' : '窗帘按钮',
unknownFunction: isEn ? 'Unknown Function' : '未知功能',
fullyOpen: isEn ? 'Fully Open' : '已全开',
fullyClosed: isEn ? 'Fully Closed' : '已全关',
paused: isEn ? 'Paused' : '已暂停',
openness: isEn ? `Open Level ${params.value}%` : `开合度 ${params.value}%`,
controllable: isEn ? 'Ready to Control' : '可控制',
currentValue: isEn ? `Current Value ${params.value}%` : `当前值 ${params.value}%`,
adjustable: isEn ? 'Adjustable' : '可调节',
enabled: isEn ? 'Enabled' : '已开启',
disabled: isEn ? 'Disabled' : '已关闭',
unnamedDevice: isEn ? 'Unnamed Device' : '未命名设备'
}
return map[key] || ''
}
function collectObjectArray(value, depth = 0) {
if (!value || depth > 6) {
return []
}
const normalizedValue = typeof value === 'string'
? (safeJsonParse(value, value))
: value
if (Array.isArray(normalizedValue)) {
if (normalizedValue.some(item => item && typeof item === 'object' && !Array.isArray(item))) {
return normalizedValue
}
for (let index = 0; index < normalizedValue.length; index += 1) {
const nestedList = collectObjectArray(normalizedValue[index], depth + 1)
if (nestedList.length) {
return nestedList
}
}
return []
}
if (!normalizedValue || typeof normalizedValue !== 'object') {
return []
}
const candidateKeys = ['data', 'list', 'rows', 'items', 'records', 'result', 'payload', '$values']
for (let index = 0; index < candidateKeys.length; index += 1) {
const key = candidateKeys[index]
if (!Object.prototype.hasOwnProperty.call(normalizedValue, key)) {
continue
}
const nestedList = collectObjectArray(normalizedValue[key], depth + 1)
if (nestedList.length) {
return nestedList
}
}
return []
}
function isSmartDeviceObject(value = {}) {
if (!value || typeof value !== 'object' || Array.isArray(value)) {
return false
}
if (Array.isArray(value.functions)) {
return true
}
const deviceKeys = [
'deviceId',
'deviceNo',
'deviceName',
'platformDeviceId',
'deviceType',
'facilityCode',
'facilityType',
'currentStatus'
]
return deviceKeys.some(key => Object.prototype.hasOwnProperty.call(value, key))
}
function unwrapSmartDevicePayload(value, depth = 0) {
if (!value || depth > 6) {
return {}
}
const normalizedValue = typeof value === 'string'
? (safeJsonParse(value, value))
: value
if (!normalizedValue || typeof normalizedValue !== 'object') {
return {}
}
if (Array.isArray(normalizedValue)) {
for (let index = 0; index < normalizedValue.length; index += 1) {
const nestedDevice = unwrapSmartDevicePayload(normalizedValue[index], depth + 1)
if (Object.keys(nestedDevice).length) {
return nestedDevice
}
}
return {}
}
if (isSmartDeviceObject(normalizedValue)) {
return normalizedValue
}
const candidateKeys = ['data', 'result', 'payload', 'item', 'device', 'record']
for (let index = 0; index < candidateKeys.length; index += 1) {
const key = candidateKeys[index]
if (!Object.prototype.hasOwnProperty.call(normalizedValue, key)) {
continue
}
const nestedDevice = unwrapSmartDevicePayload(normalizedValue[key], depth + 1)
if (Object.keys(nestedDevice).length) {
return nestedDevice
}
}
return {}
}
function isCurtainDeviceLike(device = {}) {
const numericDeviceType = Number(device.deviceType)
if (numericDeviceType === 2) {
return true
}
const name = pickFirstFilledValue(device.deviceName, device.deviceNo).toLowerCase()
return name.includes('窗帘') || name.includes('curtain')
}
function normalizeFunctionCode(functionCode = '', device = {}) {
const normalizedCode = String(functionCode || '').trim().toLowerCase()
if (!normalizedCode) {
return ''
}
if (['light', 'switch', 'common:switch'].includes(normalizedCode)) {
return 'common:switch'
}
if (['slide', 'common:slide'].includes(normalizedCode)) {
return 'common:slide'
}
if (['curtain', 'curtain:button'].includes(normalizedCode)) {
return 'curtain:button'
}
if (normalizedCode === 'button') {
return isCurtainDeviceLike(device) ? 'curtain:button' : 'button'
}
return normalizedCode
}
function normalizeFunctionItem(item = {}, device = {}) {
const schema = item.commandSchema === undefined
? null
: (typeof item.commandSchema === 'string'
? (safeJsonParse(item.commandSchema, item.commandSchema))
: item.commandSchema)
const rawFunctionCode = pickFirstFilledValue(item.functionCode, item.controlType)
const normalizedCode = normalizeFunctionCode(rawFunctionCode, device)
return {
...item,
functionCode: pickFirstFilledValue(item.functionCode, item.controlType),
functionDes: pickFirstFilledValue(item.functionDes, item.description),
controlType: normalizedCode,
commandSchema: schema
}
}
function normalizeSchemaOption(option, index = 0) {
if (option && typeof option === 'object' && !Array.isArray(option)) {
const value = Object.prototype.hasOwnProperty.call(option, 'value')
? option.value
: (
Object.prototype.hasOwnProperty.call(option, 'code')
? option.code
: (Object.prototype.hasOwnProperty.call(option, 'key') ? option.key : index)
)
return {
label: pickFirstFilledValue(option.label, option.text, option.name, option.title, value),
value
}
}
return {
label: String(option),
value: option
}
}
function normalizeSchemaOptions(rawOptions) {
if (!Array.isArray(rawOptions)) {
return []
}
return rawOptions.map((item, index) => normalizeSchemaOption(item, index))
}
function isSchemaMetaKey(key = '') {
return [
'type',
'title',
'description',
'required',
'$schema',
'properties',
'fields',
'params',
'definitions'
].includes(String(key || '').trim())
}
function inferSchemaFieldType(fieldName = '', rawField) {
if (Array.isArray(rawField)) {
return 'enum'
}
if (typeof rawField === 'string') {
const normalizedType = rawField.trim().toLowerCase()
if (['boolean', 'bool'].includes(normalizedType)) {
return 'boolean'
}
if (['number', 'integer', 'int', 'float', 'double'].includes(normalizedType)) {
return 'number'
}
}
if (!rawField || typeof rawField !== 'object') {
return typeof rawField === 'boolean' ? 'boolean' : (typeof rawField === 'number' ? 'number' : 'string')
}
const explicitType = String(
rawField.type
|| rawField.valueType
|| rawField.inputType
|| rawField.dataType
|| ''
).trim().toLowerCase()
if (rawField.enum || rawField.options || rawField.oneOf || rawField.values) {
return 'enum'
}
if (['boolean', 'bool'].includes(explicitType)) {
return 'boolean'
}
if (['number', 'integer', 'int', 'float', 'double'].includes(explicitType)) {
return 'number'
}
if (['string', 'text'].includes(explicitType)) {
return 'string'
}
if (typeof rawField.default === 'boolean') {
return 'boolean'
}
if (typeof rawField.default === 'number') {
return 'number'
}
if (fieldName === 'value' && typeof rawField === 'object' && !Array.isArray(rawField)) {
return 'number'
}
return 'string'
}
function normalizeSchemaField(fieldName = '', rawField = {}) {
const normalizedType = inferSchemaFieldType(fieldName, rawField)
const fieldObject = rawField && typeof rawField === 'object' && !Array.isArray(rawField)
? rawField
: {}
const optionCandidates = fieldObject.enum || fieldObject.options || fieldObject.oneOf || fieldObject.values || rawField
return {
name: String(fieldName || '').trim(),
label: pickFirstFilledValue(
fieldObject.label,
fieldObject.title,
fieldObject.name,
fieldObject.description,
fieldName
),
type: normalizedType,
required: !!fieldObject.required,
min: Number.isFinite(Number(fieldObject.min))
? Number(fieldObject.min)
: (Number.isFinite(Number(fieldObject.minimum)) ? Number(fieldObject.minimum) : undefined),
max: Number.isFinite(Number(fieldObject.max))
? Number(fieldObject.max)
: (Number.isFinite(Number(fieldObject.maximum)) ? Number(fieldObject.maximum) : undefined),
step: Number.isFinite(Number(fieldObject.step))
? Number(fieldObject.step)
: (Number.isFinite(Number(fieldObject.multipleOf)) ? Number(fieldObject.multipleOf) : 1),
placeholder: pickFirstFilledValue(fieldObject.placeholder, fieldObject.hint, fieldObject.description),
defaultValue: Object.prototype.hasOwnProperty.call(fieldObject, 'default') ? fieldObject.default : undefined,
options: normalizeSchemaOptions(optionCandidates)
}
}
function parseCurrentStatus(value) {
if (value === undefined || value === null) {
return null
}
if (typeof value === 'object') {
return value
}
const parsedJson = safeJsonParse(value, null)
if (parsedJson !== null) {
return parsedJson
}
const text = String(value).trim()
if (!text) {
return null
}
if (text === 'true' || text === 'false') {
return {
value: text === 'true'
}
}
const numericValue = Number(text)
if (Number.isFinite(numericValue) && String(numericValue) === text) {
return {
value: numericValue
}
}
return {
text
}
}
function resolveFunctionCurrentStatus(device = {}, preferredControlType = '') {
const functionList = Array.isArray(device.functions) ? device.functions : []
if (!functionList.length) {
return null
}
const preferredCategory = getControlTypeCategory(preferredControlType)
const preferredFunction = functionList.find((item) => {
const itemControlType = pickFirstFilledValue(item && item.controlType, item && item.functionCode)
return getControlTypeCategory(itemControlType) === preferredCategory && parseCurrentStatus(item && item.currentStatus)
})
if (preferredFunction) {
return preferredFunction.currentStatus
}
const fallbackFunction = functionList.find(item => parseCurrentStatus(item && item.currentStatus))
return fallbackFunction ? fallbackFunction.currentStatus : null
}
function resolveDeviceCurrentStatus(device = {}, preferredControlType = '') {
const deviceStatus = parseCurrentStatus(device.currentStatus)
if (deviceStatus) {
return device.currentStatus
}
return resolveFunctionCurrentStatus(device, preferredControlType)
}
const CONTROL_TYPE_CATEGORY_MAP = {
light: 'switch',
switch: 'switch',
'common:switch': 'switch',
slide: 'slide',
'common:slide': 'slide',
curtain: 'curtain',
'curtain:button': 'curtain'
}
export const SUPPORTED_DEVICE_CONTROL_TYPES = ['common:switch', 'common:slide', 'curtain:button', 'light', 'switch', 'curtain']
export function getControlTypeCategory(controlType = '') {
const normalizedType = String(controlType || '').trim().toLowerCase()
return CONTROL_TYPE_CATEGORY_MAP[normalizedType] || normalizedType
}
export function isSupportedDeviceControlType(controlType = '') {
const normalizedType = String(controlType || '').trim().toLowerCase()
return SUPPORTED_DEVICE_CONTROL_TYPES.includes(normalizedType) || ['switch', 'slide', 'curtain'].includes(getControlTypeCategory(normalizedType))
}
export function getDeviceControlLabel(controlType = '') {
const category = getControlTypeCategory(controlType)
if (category === 'switch') {
return getSmartDeviceText('commonSwitch')
}
if (category === 'slide') {
return getSmartDeviceText('slideControl')
}
if (category === 'curtain') {
return getSmartDeviceText('curtainButton')
}
return String(controlType || '').trim() || getSmartDeviceText('unknownFunction')
}
export function inferDeviceControlType(device = {}) {
const numericDeviceType = Number(device.deviceType)
if (numericDeviceType === 2) {
return 'curtain:button'
}
const functionList = Array.isArray(device.functions) ? device.functions : []
const normalizedFunctionTypes = functionList
.map(item => normalizeFunctionCode(
pickFirstFilledValue(item && item.functionCode, item && item.controlType),
device
))
.filter(Boolean)
if (normalizedFunctionTypes.some(item => getControlTypeCategory(item) === 'switch')) {
return 'common:switch'
}
if (normalizedFunctionTypes.some(item => getControlTypeCategory(item) === 'slide')) {
return 'common:slide'
}
if (normalizedFunctionTypes.some(item => getControlTypeCategory(item) === 'curtain')) {
return 'curtain:button'
}
if (numericDeviceType === 1 || numericDeviceType === 0) {
return 'common:switch'
}
const name = pickFirstFilledValue(device.deviceName, device.deviceNo).toLowerCase()
if (name.includes('窗帘') || name.includes('curtain')) {
return 'curtain:button'
}
if (name.includes('开关') || name.includes('switch')) {
return 'common:switch'
}
if (name.includes('亮度') || name.includes('滑块') || name.includes('slider') || name.includes('slide')) {
return 'common:slide'
}
return 'common:switch'
}
export function getDeviceIconClass(controlType = '') {
const category = getControlTypeCategory(controlType)
if (category === 'curtain') {
return 'tn-icon-hardware-fill'
}
if (category === 'slide') {
return 'tn-icon-more-horizontal'
}
if (category === 'switch') {
return 'tn-icon-power'
}
return 'tn-icon-light-fill'
}
export function buildDeviceStateText(device = {}) {
const controlType = inferDeviceControlType(device)
const category = getControlTypeCategory(controlType)
const status = parseCurrentStatus(resolveDeviceCurrentStatus(device, controlType))
if (category === 'curtain' || Number(device.deviceType) === 2) {
const action = pickFirstFilledValue(
status && status.command,
status && status.action,
status && status.state,
status && status.text
).toLowerCase()
const percentValue = Number(
status && (
status.value !== undefined
? status.value
: (status.percent !== undefined ? status.percent : status.position)
)
)
if (action === 'open') {
return getSmartDeviceText('fullyOpen')
}
if (action === 'close') {
return getSmartDeviceText('fullyClosed')
}
if (action === 'stop') {
return getSmartDeviceText('paused')
}
if (Number.isFinite(percentValue)) {
return getSmartDeviceText('openness', { value: percentValue })
}
return getSmartDeviceText('controllable')
}
if (category === 'slide') {
const slideValue = Number(
status && (
status.value !== undefined
? status.value
: (status.percent !== undefined ? status.percent : status.position)
)
)
if (Number.isFinite(slideValue)) {
return getSmartDeviceText('currentValue', { value: slideValue })
}
return getSmartDeviceText('adjustable')
}
const switchValue = status && status.value
if (typeof switchValue === 'boolean') {
return switchValue ? getSmartDeviceText('enabled') : getSmartDeviceText('disabled')
}
const switchCommand = String(
(status && (
status.command !== undefined
? status.command
: (status.action !== undefined ? status.action : status.text)
)) || ''
).trim().toLowerCase()
if (switchCommand === 'on') {
return getSmartDeviceText('enabled')
}
if (switchCommand === 'off') {
return getSmartDeviceText('disabled')
}
if (status && status.text) {
return String(status.text)
}
return getSmartDeviceText('controllable')
}
export function isDeviceActive(device = {}) {
const controlType = inferDeviceControlType(device)
const category = getControlTypeCategory(controlType)
const status = parseCurrentStatus(resolveDeviceCurrentStatus(device, controlType))
if (!status) {
return false
}
if (category === 'curtain' || Number(device.deviceType) === 2) {
const action = pickFirstFilledValue(
status.command,
status.action,
status.state,
status.text
).toLowerCase()
const value = Number(
status.value !== undefined
? status.value
: (status.percent !== undefined ? status.percent : status.position)
)
if (action === 'open') {
return true
}
if (action === 'close') {
return false
}
if (Number.isFinite(value)) {
return value > 0
}
return false
}
if (category === 'slide') {
const value = Number(
status.value !== undefined
? status.value
: (status.percent !== undefined ? status.percent : status.position)
)
return Number.isFinite(value) ? value > 0 : false
}
const switchCommand = String(
(
status.command !== undefined
? status.command
: (status.action !== undefined ? status.action : status.text)
) || ''
).trim().toLowerCase()
if (switchCommand === 'on') {
return true
}
if (switchCommand === 'off') {
return false
}
return !!status.value
}
export function normalizeSmartDevice(item = {}) {
const sourceDevice = unwrapSmartDevicePayload(item)
const functionList = Array.isArray(sourceDevice.functions)
? sourceDevice.functions.filter(entry => entry && typeof entry === 'object').map(entry => normalizeFunctionItem(entry, sourceDevice))
: []
const normalizedDevice = {
...sourceDevice,
functions: functionList
}
const controlType = inferDeviceControlType(normalizedDevice)
const currentStatus = resolveDeviceCurrentStatus(normalizedDevice, controlType)
return {
...normalizedDevice,
currentStatus,
controlType,
iconClass: getDeviceIconClass(controlType),
active: isDeviceActive(normalizedDevice),
stateText: buildDeviceStateText(normalizedDevice),
roomName: pickFirstFilledValue(sourceDevice.roomName),
deviceName: pickFirstFilledValue(sourceDevice.deviceName, sourceDevice.deviceNo, sourceDevice.platformDeviceId, getSmartDeviceText('unnamedDevice'))
}
}
export function getDeviceFunctions(device = {}, includeFallback = true) {
const functionList = Array.isArray(device.functions)
? device.functions.filter(item => item && typeof item === 'object').map(item => normalizeFunctionItem(item, device))
: []
if (functionList.length) {
return functionList
}
if (!includeFallback) {
return []
}
const fallbackType = inferDeviceControlType(device)
return fallbackType
? [{
functionCode: fallbackType,
functionDes: getDeviceControlLabel(fallbackType),
controlType: fallbackType,
commandSchema: null,
isFallback: true
}]
: []
}
export function buildCommandSchemaFields(commandSchema = null) {
if (!commandSchema) {
return []
}
if (Array.isArray(commandSchema)) {
return commandSchema
.map((item, index) => normalizeSchemaField(
pickFirstFilledValue(item && item.name, item && item.key, `field${index + 1}`),
item
))
.filter(item => item.name)
}
if (typeof commandSchema !== 'object') {
return []
}
let schemaSource = null
if (commandSchema.properties && typeof commandSchema.properties === 'object') {
schemaSource = commandSchema.properties
} else if (commandSchema.fields && typeof commandSchema.fields === 'object') {
schemaSource = commandSchema.fields
} else if (commandSchema.params && typeof commandSchema.params === 'object') {
schemaSource = commandSchema.params
} else {
const candidateEntries = Object.keys(commandSchema)
.filter(key => !isSchemaMetaKey(key))
.filter(key => {
const value = commandSchema[key]
return value !== undefined && value !== null && typeof value !== 'function'
})
if (candidateEntries.length) {
schemaSource = candidateEntries.reduce((result, key) => {
result[key] = commandSchema[key]
return result
}, {})
}
}
if (!schemaSource || typeof schemaSource !== 'object') {
return []
}
return Object.keys(schemaSource)
.map(key => normalizeSchemaField(key, schemaSource[key]))
.filter(item => item.name)
}
export function normalizeSmartDeviceList(response) {
const payload = response && response.data !== undefined ? response.data : response
const deviceList = collectObjectArray(payload)
if (deviceList.length) {
return deviceList
.filter(item => item && typeof item === 'object' && !Array.isArray(item))
.map(item => normalizeSmartDevice(item))
}
const singleDevice = unwrapSmartDevicePayload(payload)
return Object.keys(singleDevice).length ? [normalizeSmartDevice(singleDevice)] : []
}
export function patchDeviceStatus(device = {}, command = {}, forcedControlType = '') {
const controlType = String(forcedControlType || '').trim().toLowerCase() || inferDeviceControlType(device)
const category = getControlTypeCategory(controlType)
if (category === 'curtain') {
const action = String(command.command || command.action || command.value || '').trim().toLowerCase()
if (!action) {
return normalizeSmartDevice(device)
}
return normalizeSmartDevice({
...device,
currentStatus: {
command: action
}
})
}
if (category === 'slide') {
const value = Number(
command.value !== undefined
? command.value
: (command.percent !== undefined ? command.percent : command.position)
)
return normalizeSmartDevice({
...device,
currentStatus: {
value: Number.isFinite(value) ? value : 0
}
})
}
const switchCommand = String(
(
command.command !== undefined
? command.command
: (
command.action !== undefined
? command.action
: (
command.value === true
? 'on'
: (command.value === false ? 'off' : command.value)
)
)
) || ''
).trim().toLowerCase()
if (switchCommand) {
return normalizeSmartDevice({
...device,
currentStatus: {
command: switchCommand
}
})
}
return normalizeSmartDevice(device)
}