feat(B5-B7): 引入 SpecSemanticResolver 作为物模型语义唯一权威
新增 SpecSemanticResolver (356行): - 语义标签体系: power/switch/backlight/indicator/brightness/color_temp/ mode/scene/countdown/child_lock/temperature/humidity/battery/motion/ contact/fault/raw - 功能分组 grp: switch/scene/mode/countdown/display/config/measure/status - resolve(): 单功能点语义推导(非主控词优先 -> 功能语义 -> 开关兜底) - pickPrimary(): 产品范围内选主开关(power优先 -> switch组首个 -> 唯一可写bool兜底) - isReadOnly(): 只读语义修正, 修复涂鸦 fnCodes 为空时兜底'全部可写'的缺陷 实体/DTO 补齐语义字段(V19 建了列但代码里没有): - ProductProperty: semantic/grp/isPrimary/channel/completionMode/enumLabel - ProductAction: completionMode - ControlItem: semantic/grp/isPrimary/enumLabels ControlSchemaServiceImpl 改造: - toItem(): 搬运语义字段; 物模型无语义时即时推导(旧数据兼容); 状态量被误标 write 时强制只读 - pickMainSwitch(): 优先 is_primary -> semantic=power -> 回退旧启发式 (原实现按名称含 switch/power 猜测, 是 JD2BTSA 背光误控的根因) - fillValues(): code 优先、dp_id 兜底(修复 dps key 语义分裂导致回填失败) ProductServiceImpl.reconcileSpecFromTuya: - 同步物模型时落库 semantic/grp, 并用语义修正 access - 新增 applyPrimary(): 选主开关并回填 is_primary 验证: mvn -pl iot-common,iot-device -am compile 通过
This commit is contained in:
parent
9786286ad3
commit
6461acecd1
@ -53,4 +53,18 @@ public class ControlItem {
|
||||
|
||||
@Schema(description = "当前值(按设备查询时填充)")
|
||||
private Object value;
|
||||
|
||||
// ===== 语义扩展(客户端据此渲染,取代按名称猜测)=====
|
||||
|
||||
@Schema(description = "功能语义标签(power/switch/backlight/indicator/brightness/color_temp/mode/scene/countdown/...)")
|
||||
private String semantic;
|
||||
|
||||
@Schema(description = "功能分组(switch/scene/mode/countdown/display/config/measure/status)")
|
||||
private String grp;
|
||||
|
||||
@Schema(description = "是否主开关(1是,0否)")
|
||||
private Integer isPrimary;
|
||||
|
||||
@Schema(description = "枚举中文名映射")
|
||||
private java.util.Map<String, String> enumLabels;
|
||||
}
|
||||
|
||||
@ -29,4 +29,7 @@ public class ProductAction extends BaseEntity {
|
||||
|
||||
@Schema(description = "输出参数(JSON)")
|
||||
private String output;
|
||||
|
||||
@Schema(description = "完成模式(sync 同步完成 / ack_only 仅受理)")
|
||||
private String completionMode;
|
||||
}
|
||||
|
||||
@ -53,4 +53,24 @@ public class ProductProperty extends BaseEntity {
|
||||
|
||||
@Schema(description = "排序")
|
||||
private Integer sort;
|
||||
|
||||
// ===== 语义扩展(V19 建列 / V22 补列,此前库里有列但代码没用)=====
|
||||
|
||||
@Schema(description = "功能语义标签(power/switch/backlight/indicator/brightness/color_temp/mode/scene/countdown/child_lock/temperature/humidity/battery/motion/contact/fault/raw)")
|
||||
private String semantic;
|
||||
|
||||
@Schema(description = "功能分组(switch/scene/mode/countdown/display/config/measure/status)")
|
||||
private String grp;
|
||||
|
||||
@Schema(description = "是否主开关(1是,0否)")
|
||||
private Integer isPrimary;
|
||||
|
||||
@Schema(description = "物理通道")
|
||||
private String channel;
|
||||
|
||||
@Schema(description = "完成模式(sync/ack_only)")
|
||||
private String completionMode;
|
||||
|
||||
@Schema(description = "枚举中文名(JSON对象)")
|
||||
private String enumLabel;
|
||||
}
|
||||
|
||||
@ -17,9 +17,11 @@ import com.iot.device.mapper.ProductEventMapper;
|
||||
import com.iot.device.mapper.ProductMapper;
|
||||
import com.iot.device.mapper.ProductPropertyMapper;
|
||||
import com.iot.device.service.ControlSchemaService;
|
||||
import com.iot.device.service.spec.SpecSemanticResolver;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
@ -42,6 +44,7 @@ public class ControlSchemaServiceImpl implements ControlSchemaService {
|
||||
private final ProductEventMapper productEventMapper;
|
||||
private final DeviceMapper deviceMapper;
|
||||
private final ObjectMapper objectMapper;
|
||||
private final SpecSemanticResolver semanticResolver;
|
||||
|
||||
@Override
|
||||
public ControlSchema getProductSchema(Long productId) {
|
||||
@ -134,8 +137,19 @@ public class ControlSchemaServiceImpl implements ControlSchemaService {
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* 回填当前值。
|
||||
*
|
||||
* <p><b>key 语义统一</b>:物模型 code 是平台无关的稳定标识(涂鸦 code),
|
||||
* 而 dp_id 是涂鸦的数字 DP 别名。运行期上报的 dps 的 key 两者都可能出现
|
||||
* (APP 上报用数字 dpId,云端状态同步用 code),因此按 code 优先、dp_id 兜底匹配。</p>
|
||||
*/
|
||||
private void fillValues(List<ControlItem> items, Map<String, Object> dps) {
|
||||
for (ControlItem item : items) {
|
||||
if (item.getCode() != null && dps.containsKey(item.getCode())) {
|
||||
item.setValue(dps.get(item.getCode()));
|
||||
continue;
|
||||
}
|
||||
if (item.getDpId() != null && dps.containsKey(item.getDpId())) {
|
||||
item.setValue(dps.get(item.getDpId()));
|
||||
}
|
||||
@ -172,10 +186,44 @@ public class ControlSchemaServiceImpl implements ControlSchemaService {
|
||||
item.setEnumValues(parseEnum(p.getEnumList()));
|
||||
String access = p.getAccess() == null ? "" : p.getAccess().toLowerCase(Locale.ROOT);
|
||||
item.setWritable(access.contains("write"));
|
||||
// 语义字段:物模型已落库则直接用;未落库(旧数据)则即时推导,保证客户端始终拿得到语义
|
||||
String semantic = p.getSemantic();
|
||||
String grp = p.getGrp();
|
||||
if (!StringUtils.hasText(semantic) || !StringUtils.hasText(grp)) {
|
||||
SpecSemanticResolver.Semantic s = semanticResolver.resolve(
|
||||
p.getCode(), p.getName(), p.getType(), p.getAccess());
|
||||
if (!StringUtils.hasText(semantic)) {
|
||||
semantic = s.getSemantic();
|
||||
}
|
||||
if (!StringUtils.hasText(grp)) {
|
||||
grp = s.getGrp();
|
||||
}
|
||||
// 可写性修正:状态量被误标 write 时强制只读
|
||||
if (semanticResolver.isReadOnly(semantic, grp, p.getCode())) {
|
||||
item.setWritable(false);
|
||||
}
|
||||
}
|
||||
item.setSemantic(semantic);
|
||||
item.setGrp(grp);
|
||||
item.setIsPrimary(p.getIsPrimary());
|
||||
item.setEnumLabels(parseEnumLabels(p.getEnumLabel()));
|
||||
item.setWidget(resolveWidget(item));
|
||||
return item;
|
||||
}
|
||||
|
||||
/** 解析枚举中文名映射({"warm":"暖风"})。 */
|
||||
private Map<String, String> parseEnumLabels(String json) {
|
||||
if (json == null || json.isBlank()) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
return objectMapper.readValue(json, new TypeReference<Map<String, String>>() {
|
||||
});
|
||||
} catch (Exception e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private String resolveWidget(ControlItem item) {
|
||||
String type = item.getType() == null ? "" : item.getType().toLowerCase(Locale.ROOT);
|
||||
String name = item.getName() == null ? "" : item.getName().toLowerCase(Locale.ROOT);
|
||||
@ -195,8 +243,28 @@ public class ControlSchemaServiceImpl implements ControlSchemaService {
|
||||
return "text";
|
||||
}
|
||||
|
||||
/** 主开关:优先匹配 开关/电源/switch/power;否则唯一可写 bool */
|
||||
/**
|
||||
* 主开关识别。
|
||||
*
|
||||
* <p><b>重构</b>:原实现按名称含 switch/power/开关/电源 猜测,
|
||||
* 会把「背光开关」误判成主开关(JD2BTSA 误控背光的根因)。
|
||||
* 现改为优先使用物模型落库的 {@code is_primary} / {@code semantic},
|
||||
* 由 {@link SpecSemanticResolver} 单一权威判定;物模型无语义字段时才回退旧的启发式。</p>
|
||||
*/
|
||||
private String pickMainSwitch(List<ControlItem> writable) {
|
||||
// 1) 物模型已显式标记主开关
|
||||
for (ControlItem i : writable) {
|
||||
if (i.getIsPrimary() != null && i.getIsPrimary() == 1) {
|
||||
return i.getCode();
|
||||
}
|
||||
}
|
||||
// 2) 语义为 power
|
||||
for (ControlItem i : writable) {
|
||||
if (SpecSemanticResolver.POWER.equals(i.getSemantic()) && i.isWritable()) {
|
||||
return i.getCode();
|
||||
}
|
||||
}
|
||||
// 3) 回退:旧启发式(仅在物模型无语义字段时才会走到)
|
||||
List<ControlItem> bools = writable.stream()
|
||||
.filter(i -> "switch".equals(i.getWidget()))
|
||||
.toList();
|
||||
|
||||
@ -3,6 +3,8 @@ package com.iot.device.service.impl;
|
||||
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
||||
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
|
||||
import com.iot.common.constants.PlatformType;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import com.iot.device.dto.ProductSpec;
|
||||
import com.iot.device.entity.Device;
|
||||
import com.iot.device.entity.Product;
|
||||
@ -16,6 +18,7 @@ import com.iot.device.mapper.ProductMapper;
|
||||
import com.iot.device.mapper.ProductPropertyMapper;
|
||||
import com.iot.device.service.ProductService;
|
||||
import com.iot.device.service.TuyaCloudService;
|
||||
import com.iot.device.service.spec.SpecSemanticResolver;
|
||||
import com.fasterxml.jackson.databind.JsonNode;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.stereotype.Service;
|
||||
@ -38,6 +41,7 @@ public class ProductServiceImpl implements ProductService {
|
||||
private final ProductActionMapper productActionMapper;
|
||||
private final DeviceMapper deviceMapper;
|
||||
private final TuyaCloudService tuyaCloudService;
|
||||
private final SpecSemanticResolver semanticResolver;
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
@ -327,8 +331,19 @@ public class ProductServiceImpl implements ProductService {
|
||||
}
|
||||
}
|
||||
pp.setSort(sort++);
|
||||
// 语义落库(SpecSemanticResolver 为唯一权威):
|
||||
// 1) semantic / grp 决定客户端如何渲染、能否当主开关;
|
||||
// 2) 修正 access —— 涂鸦 fnCodes 为空时兜底成"全部可写",
|
||||
// 会把 scene_1~6 / relay_status / countdown 等状态量误标为可写。
|
||||
SpecSemanticResolver.Semantic sem = semanticResolver.resolve(
|
||||
pp.getCode(), pp.getName(), pp.getType(), pp.getAccess());
|
||||
pp.setSemantic(sem.getSemantic());
|
||||
pp.setGrp(sem.getGrp());
|
||||
pp.setAccess(sem.isWritable() ? "write" : "read");
|
||||
productPropertyMapper.insert(pp);
|
||||
}
|
||||
// 选主开关并回填 is_primary(详情页大圆钮的唯一依据)
|
||||
applyPrimary(p.getId());
|
||||
p.setSpecVersion(p.getSpecVersion() == null ? 1 : p.getSpecVersion() + 1);
|
||||
productMapper.updateById(p);
|
||||
total++;
|
||||
@ -336,6 +351,37 @@ public class ProductServiceImpl implements ProductService {
|
||||
return total;
|
||||
}
|
||||
|
||||
/**
|
||||
* 依据语义规则选出产品的主开关功能点,并回填 is_primary。
|
||||
*/
|
||||
private void applyPrimary(Long productId) {
|
||||
List<ProductProperty> props = productPropertyMapper.selectList(
|
||||
new LambdaQueryWrapper<ProductProperty>().eq(ProductProperty::getProductId, productId));
|
||||
if (props.isEmpty()) {
|
||||
return;
|
||||
}
|
||||
List<SpecSemanticResolver.ProductPropertyView> views = new ArrayList<>();
|
||||
for (ProductProperty pp : props) {
|
||||
SpecSemanticResolver.Semantic sem = semanticResolver.resolve(
|
||||
pp.getCode(), pp.getName(), pp.getType(), pp.getAccess());
|
||||
views.add(new SpecSemanticResolver.ProductPropertyView(
|
||||
pp.getCode(), pp.getName(), pp.getType(),
|
||||
pp.getSemantic() != null ? pp.getSemantic() : sem.getSemantic(),
|
||||
pp.getGrp() != null ? pp.getGrp() : sem.getGrp(),
|
||||
sem.isWritable(), pp.getSort()));
|
||||
}
|
||||
String primary = semanticResolver.pickPrimary(views);
|
||||
for (ProductProperty pp : props) {
|
||||
int want = (primary != null && primary.equals(pp.getCode())) ? 1 : 0;
|
||||
if (pp.getIsPrimary() == null || pp.getIsPrimary() != want) {
|
||||
ProductProperty upd = new ProductProperty();
|
||||
upd.setId(pp.getId());
|
||||
upd.setIsPrimary(want);
|
||||
productPropertyMapper.updateById(upd);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private String mapTuyaType(String t) {
|
||||
if (t == null) {
|
||||
return "string";
|
||||
|
||||
@ -0,0 +1,413 @@
|
||||
package com.iot.device.service.spec;
|
||||
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
/**
|
||||
* 物模型语义解析器 —— 功能点语义规则的唯一权威。
|
||||
*
|
||||
* <p>重构前,同一个"哪个功能点是主开关、哪些只是状态量"的问题有两份启发式实现:</p>
|
||||
* <ul>
|
||||
* <li>Android 端 TuyaSdkService.switchDpScore / isNonPowerSwitchName
|
||||
* (26 个中英排除词 + 100/90/60/40/10 五档打分)</li>
|
||||
* <li>后端 ControlSchemaServiceImpl.pickMainSwitch
|
||||
* (名称含 switch/power/开关/电源)</li>
|
||||
* </ul>
|
||||
* <p>两份实现口径不一致,是 JD2BTSA「背光开关被当成主开关误控」以及
|
||||
* JD6BTSA「scene_1~6 / countdown / relay_status 等状态量被渲染成可写控件」的直接根因。
|
||||
* 现统一收敛到本类,客户端只消费物模型落库的 semantic / grp / is_primary 字段。</p>
|
||||
*/
|
||||
@Component
|
||||
public class SpecSemanticResolver {
|
||||
|
||||
// ==================== 语义标签常量 ====================
|
||||
public static final String POWER = "power";
|
||||
public static final String SWITCH = "switch";
|
||||
public static final String BACKLIGHT = "backlight";
|
||||
public static final String INDICATOR = "indicator";
|
||||
public static final String BRIGHTNESS = "brightness";
|
||||
public static final String COLOR_TEMP = "color_temp";
|
||||
public static final String MODE = "mode";
|
||||
public static final String SCENE = "scene";
|
||||
public static final String COUNTDOWN = "countdown";
|
||||
public static final String CHILD_LOCK = "child_lock";
|
||||
public static final String TEMPERATURE = "temperature";
|
||||
public static final String HUMIDITY = "humidity";
|
||||
public static final String BATTERY = "battery";
|
||||
public static final String MOTION = "motion";
|
||||
public static final String CONTACT = "contact";
|
||||
public static final String FAULT = "fault";
|
||||
public static final String RAW = "raw";
|
||||
|
||||
// ==================== 功能分组常量 ====================
|
||||
public static final String GRP_SWITCH = "switch";
|
||||
public static final String GRP_SCENE = "scene";
|
||||
public static final String GRP_MODE = "mode";
|
||||
public static final String GRP_COUNTDOWN = "countdown";
|
||||
public static final String GRP_DISPLAY = "display";
|
||||
public static final String GRP_CONFIG = "config";
|
||||
public static final String GRP_MEASURE = "measure";
|
||||
public static final String GRP_STATUS = "status";
|
||||
|
||||
/** 非主控语义词表:命中即判定为状态量/配置项,永远不能被当作主开关。 */
|
||||
private static final Map<String, String> NON_CONTROL = buildNonControl();
|
||||
|
||||
/** 功能语义词表:命中即给出功能语义。 */
|
||||
private static final Map<String, String> FUNCTION = buildFunction();
|
||||
|
||||
/**
|
||||
* 只读语义集合:即使物模型里被标成 write,也应视为只读状态量。
|
||||
* 根因:涂鸦物模型同步时用 fnCodes.contains(code) 判定可写,
|
||||
* 而 fnCodes 为空时兜底成"全部可写",导致状态量被误标。
|
||||
*/
|
||||
private static final Set<String> READ_ONLY_SEMANTICS = Set.of(SCENE, FAULT);
|
||||
|
||||
/** 明确不可作为主开关的分组。 */
|
||||
private static final Set<String> NON_PRIMARY_GROUPS = Set.of(
|
||||
GRP_SCENE, GRP_MODE, GRP_COUNTDOWN, GRP_DISPLAY, GRP_CONFIG, GRP_MEASURE, GRP_STATUS);
|
||||
|
||||
/**
|
||||
* 解析单个功能点的语义。
|
||||
*
|
||||
* @param code 功能点编码(如 switch_1 / backlight_switch)
|
||||
* @param name 功能点名称(如 开关 / 背光开关)
|
||||
* @param type 数据类型(bool/int/enum/...)
|
||||
* @param access 声明的访问权限(read/write/notify)
|
||||
*/
|
||||
public Semantic resolve(String code, String name, String type, String access) {
|
||||
String c = norm(code);
|
||||
String n = norm(name);
|
||||
String both = (c + " " + n).trim();
|
||||
|
||||
String semantic = null;
|
||||
String grp = matchGroup(c);
|
||||
|
||||
// 1) 先判"非主控"(背光/指示灯/倒计时/状态量…)——最容易被误认成主开关
|
||||
for (Map.Entry<String, String> e : NON_CONTROL.entrySet()) {
|
||||
if (containsWord(both, e.getKey())) {
|
||||
semantic = e.getValue();
|
||||
break;
|
||||
}
|
||||
}
|
||||
// 2) 再判功能语义
|
||||
if (semantic == null) {
|
||||
for (Map.Entry<String, String> e : FUNCTION.entrySet()) {
|
||||
if (containsWord(both, e.getKey())) {
|
||||
semantic = e.getValue();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
// 3) 开关类兜底
|
||||
if (semantic == null && isSwitchWord(both)) {
|
||||
semantic = SWITCH;
|
||||
}
|
||||
if (semantic == null) {
|
||||
semantic = RAW;
|
||||
}
|
||||
|
||||
// 分组兜底
|
||||
if (grp == null) {
|
||||
grp = defaultGroup(semantic);
|
||||
}
|
||||
|
||||
// 可写性修正:声明 access 含 write,但语义属只读状态量 → 强制只读
|
||||
boolean declaredWritable = access != null && access.toLowerCase(Locale.ROOT).contains("write");
|
||||
boolean writable = declaredWritable && !isReadOnly(semantic, grp, code);
|
||||
|
||||
return new Semantic(semantic, grp, writable, false);
|
||||
}
|
||||
/**
|
||||
* 在一个产品的物模型范围内选出主开关(is_primary)。
|
||||
*
|
||||
* <p>规则:</p>
|
||||
* <ol>
|
||||
* <li>优先取语义为 power 且可写的功能点(sort 最小者);</li>
|
||||
* <li>其次取 switch 分组内 sort 最小的可写开关通道(多通道设备以首个为详情页主开关,
|
||||
* 列表页仍按 grp 整组联动);</li>
|
||||
* <li>都没有则无主开关(客户端退化为普通控件列表)。</li>
|
||||
* </ol>
|
||||
*
|
||||
* @return 主开关的 code;无主开关返回 null
|
||||
*/
|
||||
public String pickPrimary(List<ProductPropertyView> props) {
|
||||
if (props == null || props.isEmpty()) {
|
||||
return null;
|
||||
}
|
||||
List<ProductPropertyView> sorted = new ArrayList<>(props);
|
||||
sorted.sort((a, b) -> Integer.compare(nz(a.getSort()), nz(b.getSort())));
|
||||
|
||||
// 1) 明确标记为 power 的
|
||||
for (ProductPropertyView p : sorted) {
|
||||
if (POWER.equals(p.getSemantic()) && p.isWritable()) {
|
||||
return p.getCode();
|
||||
}
|
||||
}
|
||||
// 2) switch 分组内首个可写通道
|
||||
for (ProductPropertyView p : sorted) {
|
||||
if (p.isWritable()
|
||||
&& !NON_PRIMARY_GROUPS.contains(p.getGrp())
|
||||
&& (SWITCH.equals(p.getSemantic()) || GRP_SWITCH.equals(p.getGrp()))) {
|
||||
return p.getCode();
|
||||
}
|
||||
}
|
||||
// 3) 兜底:全产品唯一可写布尔
|
||||
ProductPropertyView only = null;
|
||||
int writableBoolCount = 0;
|
||||
for (ProductPropertyView p : sorted) {
|
||||
if (p.isWritable() && "bool".equalsIgnoreCase(p.getType())) {
|
||||
writableBoolCount++;
|
||||
if (only == null) {
|
||||
only = p;
|
||||
}
|
||||
}
|
||||
}
|
||||
return (writableBoolCount == 1 && only != null) ? only.getCode() : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断某语义/分组是否应视为只读状态量。
|
||||
*/
|
||||
public boolean isReadOnly(String semantic, String grp, String code) {
|
||||
if (semantic != null && READ_ONLY_SEMANTICS.contains(semantic)) {
|
||||
return true;
|
||||
}
|
||||
if (grp != null && (GRP_STATUS.equals(grp) || GRP_SCENE.equals(grp))) {
|
||||
return true;
|
||||
}
|
||||
// relay_status* 这类明确的状态上报前缀
|
||||
return code != null && code.toLowerCase(Locale.ROOT).startsWith("relay_status");
|
||||
}
|
||||
|
||||
/** 判断某分组是否为「开关通道组」(设备列表整组联动的依据)。 */
|
||||
public boolean isSwitchGroup(String grp) {
|
||||
return GRP_SWITCH.equals(grp);
|
||||
}
|
||||
|
||||
// ==================== 内部规则 ====================
|
||||
|
||||
private static Map<String, String> buildNonControl() {
|
||||
Map<String, String> m = new LinkedHashMap<>();
|
||||
// 背光 / 指示灯 / 氛围
|
||||
m.put("背光", BACKLIGHT);
|
||||
m.put("backlight", BACKLIGHT);
|
||||
m.put("指示灯", INDICATOR);
|
||||
m.put("指示", INDICATOR);
|
||||
m.put("indicator", INDICATOR);
|
||||
m.put("氛围灯", INDICATOR);
|
||||
m.put("氛围", INDICATOR);
|
||||
m.put("ambience", INDICATOR);
|
||||
m.put("夜灯", INDICATOR);
|
||||
m.put("night", INDICATOR);
|
||||
m.put("灯效", INDICATOR);
|
||||
m.put("彩灯", INDICATOR);
|
||||
m.put("led", INDICATOR);
|
||||
// 倒计时 / 定时
|
||||
m.put("倒计时", COUNTDOWN);
|
||||
m.put("countdown", COUNTDOWN);
|
||||
m.put("定时", COUNTDOWN);
|
||||
m.put("timer", COUNTDOWN);
|
||||
// 童锁
|
||||
m.put("童锁", CHILD_LOCK);
|
||||
m.put("childlock", CHILD_LOCK);
|
||||
m.put("child lock", CHILD_LOCK);
|
||||
// 故障 / 报警
|
||||
m.put("故障", FAULT);
|
||||
m.put("fault", FAULT);
|
||||
m.put("报警", FAULT);
|
||||
m.put("alarm", FAULT);
|
||||
// 其余状态/配置量:归为原始语义,绝不作为主开关
|
||||
for (String w : List.of("状态", "status", "state", "剩余", "remaining", "累计",
|
||||
"保温", "节能", "省电", "eco", "恢复", "出厂", "设置", "setting", "reset",
|
||||
"锁定", "lock", "配网", "校准")) {
|
||||
m.put(w, RAW);
|
||||
}
|
||||
return m;
|
||||
}
|
||||
|
||||
private static Map<String, String> buildFunction() {
|
||||
Map<String, String> m = new LinkedHashMap<>();
|
||||
m.put("亮度", BRIGHTNESS);
|
||||
m.put("brightness", BRIGHTNESS);
|
||||
m.put("调光", BRIGHTNESS);
|
||||
m.put("色温", COLOR_TEMP);
|
||||
m.put("colortemp", COLOR_TEMP);
|
||||
m.put("colortempvalue", COLOR_TEMP);
|
||||
m.put("模式", MODE);
|
||||
m.put("mode", MODE);
|
||||
m.put("场景", SCENE);
|
||||
m.put("scene", SCENE);
|
||||
m.put("preset", SCENE);
|
||||
m.put("温度", TEMPERATURE);
|
||||
m.put("temperature", TEMPERATURE);
|
||||
m.put("湿度", HUMIDITY);
|
||||
m.put("humidity", HUMIDITY);
|
||||
m.put("电量", BATTERY);
|
||||
m.put("battery", BATTERY);
|
||||
m.put("人体", MOTION);
|
||||
m.put("motion", MOTION);
|
||||
m.put("pir", MOTION);
|
||||
m.put("门磁", CONTACT);
|
||||
m.put("contact", CONTACT);
|
||||
return m;
|
||||
}
|
||||
|
||||
/** 分组匹配:优先按编码前缀(switch_1 → switch)。 */
|
||||
private String matchGroup(String normCode) {
|
||||
String c = normCode;
|
||||
if (c.startsWith("switch")) {
|
||||
return GRP_SWITCH;
|
||||
}
|
||||
if (c.startsWith("scene")) {
|
||||
return GRP_SCENE;
|
||||
}
|
||||
if (c.startsWith("mode")) {
|
||||
return GRP_MODE;
|
||||
}
|
||||
if (c.startsWith("countdown") || c.startsWith("timer")) {
|
||||
return GRP_COUNTDOWN;
|
||||
}
|
||||
if (c.startsWith("relaystatus")) {
|
||||
return GRP_STATUS;
|
||||
}
|
||||
if (c.startsWith("backlight")) {
|
||||
return GRP_DISPLAY;
|
||||
}
|
||||
if (c.startsWith("battery")) {
|
||||
return GRP_MEASURE;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private String defaultGroup(String semantic) {
|
||||
if (semantic == null) {
|
||||
return null;
|
||||
}
|
||||
return switch (semantic) {
|
||||
case SWITCH, POWER -> GRP_SWITCH;
|
||||
case SCENE -> GRP_SCENE;
|
||||
case MODE -> GRP_MODE;
|
||||
case COUNTDOWN -> GRP_COUNTDOWN;
|
||||
case BACKLIGHT, INDICATOR -> GRP_DISPLAY;
|
||||
case TEMPERATURE, HUMIDITY, BATTERY -> GRP_MEASURE;
|
||||
case FAULT -> GRP_STATUS;
|
||||
default -> null;
|
||||
};
|
||||
}
|
||||
|
||||
/** 归一化:小写 + 去掉空格/下划线/连字符/点。 */
|
||||
private String norm(String s) {
|
||||
if (s == null) {
|
||||
return "";
|
||||
}
|
||||
return s.trim().toLowerCase(Locale.ROOT)
|
||||
.replace(" ", "").replace("_", "").replace("-", "").replace(".", "");
|
||||
}
|
||||
|
||||
/** 词包含判断:归一化后的串里包含该词。 */
|
||||
private boolean containsWord(String normalizedBoth, String word) {
|
||||
if (word == null || word.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
String w = word.toLowerCase(Locale.ROOT).replace(" ", "");
|
||||
return normalizedBoth.contains(w);
|
||||
}
|
||||
|
||||
private boolean isSwitchWord(String both) {
|
||||
return both.contains("开关") || both.contains("switch") || both.contains("电源")
|
||||
|| both.contains("power") || both.contains("onoff") || both.contains("通电")
|
||||
|| both.contains("断电") || both.contains("供电") || both.contains("relay");
|
||||
}
|
||||
|
||||
private static int nz(Integer v) {
|
||||
return v == null ? Integer.MAX_VALUE : v;
|
||||
}
|
||||
// ==================== 值对象 ====================
|
||||
|
||||
/** 语义解析结果。 */
|
||||
public static class Semantic {
|
||||
private final String semantic;
|
||||
private final String grp;
|
||||
private final boolean writable;
|
||||
private final boolean primary;
|
||||
|
||||
public Semantic(String semantic, String grp, boolean writable, boolean primary) {
|
||||
this.semantic = semantic;
|
||||
this.grp = grp;
|
||||
this.writable = writable;
|
||||
this.primary = primary;
|
||||
}
|
||||
|
||||
public String getSemantic() {
|
||||
return semantic;
|
||||
}
|
||||
|
||||
public String getGrp() {
|
||||
return grp;
|
||||
}
|
||||
|
||||
public boolean isWritable() {
|
||||
return writable;
|
||||
}
|
||||
|
||||
public boolean isPrimary() {
|
||||
return primary;
|
||||
}
|
||||
}
|
||||
|
||||
/** 物模型功能点视图(供主开关挑选,避免直接依赖实体层)。 */
|
||||
public static class ProductPropertyView {
|
||||
private final String code;
|
||||
private final String name;
|
||||
private final String type;
|
||||
private final String semantic;
|
||||
private final String grp;
|
||||
private final boolean writable;
|
||||
private final Integer sort;
|
||||
|
||||
public ProductPropertyView(String code, String name, String type, String semantic,
|
||||
String grp, boolean writable, Integer sort) {
|
||||
this.code = code;
|
||||
this.name = name;
|
||||
this.type = type;
|
||||
this.semantic = semantic;
|
||||
this.grp = grp;
|
||||
this.writable = writable;
|
||||
this.sort = sort;
|
||||
}
|
||||
|
||||
public String getCode() {
|
||||
return code;
|
||||
}
|
||||
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public String getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public String getSemantic() {
|
||||
return semantic;
|
||||
}
|
||||
|
||||
public String getGrp() {
|
||||
return grp;
|
||||
}
|
||||
|
||||
public boolean isWritable() {
|
||||
return writable;
|
||||
}
|
||||
|
||||
public Integer getSort() {
|
||||
return sort;
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user