// ==UserScript==
// @name 米家极客版油猴插件
// @namespace http://tampermonkey.net/
// @version v0.8.6
// @description 在极客页面中,点击设备列表,调用API获取设备和规则列表,并生成设备规则映射并显示在当前页面上
// @author 王丰,sk163
// @license MIT
// @match http://*/*
// @icon data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==
// @grant GM_setValue
// @grant GM_getValue
// ==/UserScript==
(async () => {
const callAPI = (api, params) => {
return new Promise(res => editor.gateway.callAPI(api, params, res));
};
let intervalId;
let selectCardIds = '';
let defaultColor='#43ad7f7f'
let defaultWindowWidth=880;
let defaultWindowHeight=600;
let enableEnhancedDisplayLog=GM_getValue("enableEnhancedDisplayLog");
let enableAutoFitContent=GM_getValue("enableAutoFitContent");
let backgroundColor = GM_getValue("backgroundColor") ;
let windowWidth = GM_getValue("windowWidth");
let windowHeight = GM_getValue("windowHeight");
if (enableEnhancedDisplayLog === undefined || enableEnhancedDisplayLog === null || enableEnhancedDisplayLog === "") {
enableEnhancedDisplayLog = true;
}
if (enableAutoFitContent === undefined || enableAutoFitContent === null || enableAutoFitContent === "") {
enableAutoFitContent = true;
}
if (backgroundColor === undefined || backgroundColor === null || backgroundColor === "") {
backgroundColor = defaultColor;
}
if (windowWidth === undefined || windowWidth === null || windowWidth === "") {
windowWidth = defaultWindowWidth;
} else {
windowWidth = parseInt(windowWidth, 10);
if (isNaN(windowWidth) || windowWidth <= 0) {
windowWidth = defaultWindowWidth;
}
}
if (windowHeight === undefined || windowHeight === null || windowHeight === "") {
windowHeight = defaultWindowHeight;
} else {
windowHeight = parseInt(windowHeight, 10);
if (isNaN(windowHeight) || windowHeight <= 0) {
windowHeight = defaultWindowHeight;
}
}
const executeScript = async () => {
if (document.getElementById('device-rule-map')) {
return;
}
if (typeof editor === 'undefined' || typeof editor.gateway === 'undefined' || typeof editor.gateway.callAPI === 'undefined') {
console.error('editor.gateway.callAPI 方法未定义。请确保在正确的环境中运行此脚本。');
return;
}
try {
const devListResponse = await callAPI('getDevList');
const devList = devListResponse.devList;
const roomNames = Array.from(new Set(Object.values(devList).map(device => device.roomName)));
const ruleList = await callAPI('getGraphList');
let devRuleMap = {};
for (const rule of ruleList) {
const content = await callAPI('getGraph', {id: rule.id});
const dids = new Set(content.nodes.map(n => n.props?.did).filter(did => did !== undefined));
const cards = new Set(content.nodes.map(n => {
return (n.props && n.cfg) ? {did: n.props.did, oriId: n.cfg.oriId} : undefined;
}).filter(card => card !== undefined));
dids.forEach(did => {
devRuleMap[did] = devRuleMap[did] ?? [];
const cardIds = Array.from(cards)
.filter(card => card.did === did)
.map(card => card.oriId).join(',');
const tempDevRule = {
ruleId: rule.id,
cardIds: cardIds,
totalCardNum: cards.size
};
devRuleMap[did].push(tempDevRule);
});
}
const result = Object.fromEntries(
Object.entries(devRuleMap).map(([k, v]) => [
k,
{
device: {
name: devList[k]?.name ?? `did: ${k}`,
roomName: devList[k]?.roomName ?? `未知`
},
rules: v.map(r => {
const rule = ruleList.find(rr => rr.id === r.ruleId);
return {
id: r.ruleId,
cardIds: r.cardIds,
totalCardNum: r.totalCardNum,
name: rule ? rule.userData.name : 'Unknown' // 添加对未找到规则的检查
};
})
}
])
);
const container = document.createElement('div');
container.id = 'device-rule-map';
container.style.position = 'fixed';
container.style.top = '10px';
container.style.right = '10px';
container.style.width = windowWidth+'px';
container.style.height = windowHeight+'px';
container.style.overflowY = 'scroll';
container.style.backgroundColor = 'white';
container.style.border = '1px solid #ccc';
container.style.paddingTop = '50px';
container.style.zIndex = 10000;
container.style.boxShadow = '0 4px 8px rgba(0, 0, 0, 0.1)';
const topBar = document.createElement('div');
topBar.style.position = 'fixed';
topBar.style.top = '0';
topBar.style.right = '10px';
topBar.style.width = windowWidth+'px';
topBar.style.height = '50px';
topBar.style.backgroundColor = 'white';
topBar.style.zIndex = 10001;
topBar.style.boxShadow = '0 4px 8px rgba(0, 0, 0, 0.1)';
topBar.style.display = 'flex';
topBar.style.justifyContent = 'space-between';
topBar.style.alignItems = 'center';
topBar.style.padding = '0 10px';
const title = document.createElement('h1');
title.style.margin = '0';
title.textContent = '设备规则映射结果';
const buttonContainer = document.createElement('div');
buttonContainer.style.display = 'flex';
buttonContainer.style.gap = '10px';
const collapseButton = document.createElement('button');
collapseButton.textContent = '折叠';
collapseButton.onclick = () => {
if (container.style.height === windowHeight+'px') {
topBar.style.width = '350px';
container.style.height = '0px';
container.style.width = '0px';
collapseButton.textContent = '展开';
} else {
topBar.style.width = windowWidth+'px';
container.style.width = windowWidth+'px';
container.style.height = windowHeight+'px';
collapseButton.textContent = '折叠';
}
};
const closeButton = document.createElement('button');
closeButton.textContent = '关闭';
closeButton.onclick = () => document.body.removeChild(container);
buttonContainer.appendChild(collapseButton);
buttonContainer.appendChild(closeButton);
topBar.appendChild(title);
topBar.appendChild(buttonContainer);
const table = document.createElement('table');
table.border = '1';
table.cellSpacing = '0';
table.cellPadding = '5';
table.style.width = '100%';
const thead = document.createElement('thead');
const headerRow = document.createElement('tr');
const roomHeader = document.createElement('th');
const deviceHeader = document.createElement('th');
const ruleHeader = document.createElement('th');
let roomSortOrder = 'asc';
let deviceSortOrder = 'asc';
let ruleSortOrder = 'asc';
const updateSortMarkers = () => {
roomHeader.innerHTML = `房间 ${roomSortOrder === 'asc' ? '⬆️' : '⬇️'}`;
deviceHeader.innerHTML = `设备 ${deviceSortOrder === 'asc' ? '⬆️' : '⬇️'}`;
ruleHeader.innerHTML = `规则 ${ruleSortOrder === 'asc' ? '⬆️' : '⬇️'}`;
};
roomHeader.textContent = '房间';
roomHeader.style.textWrap= 'nowrap';
deviceHeader.textContent = '设备';
deviceHeader.style.textWrap = 'nowrap';
ruleHeader.textContent = '规则';
roomHeader.onclick = () => {
roomSortOrder = roomSortOrder === 'asc' ? 'desc' : 'asc';
sortTable(0, roomSortOrder);
updateSortMarkers();
};
deviceHeader.onclick = () => {
deviceSortOrder = deviceSortOrder === 'asc' ? 'desc' : 'asc';
sortTable(1, deviceSortOrder);
updateSortMarkers();
};
ruleHeader.onclick = () => {
ruleSortOrder = ruleSortOrder === 'asc' ? 'desc' : 'asc';
sortTable(2, ruleSortOrder);
updateSortMarkers();
};
headerRow.appendChild(roomHeader);
headerRow.appendChild(deviceHeader);
headerRow.appendChild(ruleHeader);
thead.appendChild(headerRow);
table.appendChild(thead);
const roomFilterSelect = document.createElement('select');
roomFilterSelect.style.marginBottom = '10px';
roomFilterSelect.style.height = '28px';
roomFilterSelect.innerHTML = `<option value="">所有房间</option>` + roomNames.map(room => `<option value="${room}">${room}</option>`).join('');
roomFilterSelect.onchange = () => {
filterTable(roomFilterSelect.value,deviceFilterInput.value, ruleFilterInput.value);
};
container.appendChild(roomFilterSelect);
const deviceFilterInput = document.createElement('input');
deviceFilterInput.type = 'text';
deviceFilterInput.placeholder = '设备筛选';
deviceFilterInput.style.width = '100px';
deviceFilterInput.style.marginBottom = '10px';
deviceFilterInput.style.marginLeft = '10px';
deviceFilterInput.style.height = '28px';
deviceFilterInput.oninput = () => {
filterTable(roomFilterSelect.value,deviceFilterInput.value, ruleFilterInput.value);
};
container.appendChild(deviceFilterInput);
const ruleFilterInput = document.createElement('input');
ruleFilterInput.type = 'text';
ruleFilterInput.placeholder = '规则筛选';
ruleFilterInput.style.width = '100px';
ruleFilterInput.style.marginBottom = '10px';
ruleFilterInput.style.marginLeft = '10px';
ruleFilterInput.style.height = '28px';
ruleFilterInput.oninput = () => {
filterTable(roomFilterSelect.value,deviceFilterInput.value, ruleFilterInput.value);
};
container.appendChild(ruleFilterInput);
const widthInput = document.createElement('input');
widthInput.type = 'text';
widthInput.placeholder = windowWidth+'px';
widthInput.style.width = '60px';
widthInput.style.marginBottom = '10px';
widthInput.style.marginLeft = '10px';
widthInput.style.height = '28px';
widthInput.oninput = () => {
windowWidth=widthInput.value;
GM_setValue("windowWidth", windowWidth);
container.style.width = windowWidth + 'px';
topBar.style.width = windowWidth + 'px';
};
const spanW = document.createElement('span');
spanW.textContent = '宽度:';
spanW.style.marginLeft = '10px';
container.appendChild(spanW);
container.appendChild(widthInput);
const heightInput = document.createElement('input');
heightInput.type = 'text';
heightInput.placeholder = windowHeight+'px';
heightInput.style.width = '60px';
heightInput.style.marginBottom = '10px';
heightInput.style.marginLeft = '10px';
heightInput.style.height = '28px';
heightInput.oninput = () => {
windowHeight=heightInput.value;
GM_setValue("windowHeight", windowHeight);
container.style.height = windowHeight + 'px';
};
const spanH = document.createElement('span');
spanH.textContent = '高度:';
spanH.style.marginLeft = '10px';
container.appendChild(spanH);
container.appendChild(heightInput);
const colorInput = document.createElement('input');
colorInput.type = 'text';
colorInput.placeholder=defaultColor;
colorInput.style.width = '80px';
colorInput.style.marginBottom = '10px';
colorInput.style.marginLeft = '10px';
colorInput.style.height = '28px';
colorInput.oninput = () => {
backgroundColor = colorInput.value;
GM_setValue("backgroundColor", backgroundColor);
};
const spanC = document.createElement('span');
spanC.textContent = '卡片颜色:';
spanC.style.marginLeft = '10px';
container.appendChild(spanC);
container.appendChild(colorInput);
const logLabel = document.createElement('label');
logLabel.htmlFor = 'highlightLogCheck';
logLabel.appendChild(document.createTextNode('日志高亮'));
logLabel.style.marginBottom = '10px';
logLabel.style.marginLeft = '10px';
container.appendChild(logLabel);
const highlightLogCheck = document.createElement('input');
highlightLogCheck.type = 'checkbox';
highlightLogCheck.id = 'highlightLogCheck';
highlightLogCheck.checked=enableEnhancedDisplayLog;
highlightLogCheck.style.marginLeft = '5px';
highlightLogCheck.onchange=function() {
enableEnhancedDisplayLog = highlightLogCheck.checked;
GM_setValue("enableEnhancedDisplayLog", enableEnhancedDisplayLog);
enhancedDisplayLog();
};
container.appendChild(highlightLogCheck);
const fitLabel = document.createElement('label');
fitLabel.htmlFor = 'highlightLogCheck';
fitLabel.appendChild(document.createTextNode('自动画布'));
fitLabel.style.marginBottom = '10px';
fitLabel.style.marginLeft = '10px';
container.appendChild(fitLabel);
const autoFitCheck = document.createElement('input');
autoFitCheck.type = 'checkbox';
autoFitCheck.id = 'autoFitCheck';
autoFitCheck.checked=enableAutoFitContent;
autoFitCheck.style.marginLeft = '5px';
autoFitCheck.onchange=function() {
enableAutoFitContent = autoFitCheck.checked;
GM_setValue("enableAutoFitContent", enableAutoFitContent);
autoFitContent();
};
container.appendChild(autoFitCheck);
const tbody = document.createElement('tbody');
Object.entries(result).forEach(([did, data]) => {
const device=data.device;
const rules=data.rules;
const row = document.createElement('tr');
const roomCell = document.createElement('td');
roomCell.textContent = device.roomName;
roomCell.style.textWrap= 'nowrap';
const deviceCell = document.createElement('td');
deviceCell.textContent = device.name;
deviceCell.style.textWrap= 'nowrap';
const ruleCell = document.createElement('td');
const host = window.location.host;
rules.forEach(rule => {
const link = document.createElement('a');
link.href = `http://${host}/#/graph/${rule.id}`;
link.target = '_self';
link.textContent = rule.name + "[" + rule.cardIds.split(',').length + "/" + rule.totalCardNum + "]";
link.onclick = () => {
window.location.hash = '#/';
selectCardIds = rule.cardIds;
};
ruleCell.appendChild(link);
ruleCell.appendChild(document.createTextNode(', '));
});
ruleCell.removeChild(ruleCell.lastChild);
row.appendChild(roomCell);
row.appendChild(deviceCell);
row.appendChild(ruleCell);
tbody.appendChild(row);
});
table.appendChild(tbody);
container.appendChild(topBar);
container.appendChild(table);
document.body.appendChild(container);
function sortTable(columnIndex, sortOrder) {
const rows = Array.from(tbody.rows);
const sortedRows = rows.sort((a, b) => {
const aText = a.cells[columnIndex].textContent;
const bText = b.cells[columnIndex].textContent;
if (sortOrder === 'asc') {
return aText.localeCompare(bText);
} else {
return bText.localeCompare(aText);
}
});
tbody.innerHTML = '';
sortedRows.forEach(row => tbody.appendChild(row));
}
updateSortMarkers();
function filterTable(roomName,deviceKeyword, ruleKeyword) {
const rows = Array.from(tbody.rows);
rows.forEach(row => {
const roomText = row.cells[0].textContent;
const deviceText = row.cells[1].textContent.toLowerCase();
const ruleText = row.cells[2].textContent.toLowerCase();
if ((roomName === '' || roomText === roomName) && deviceText.includes(deviceKeyword.toLowerCase()) && ruleText.includes(ruleKeyword.toLowerCase())) {
row.style.display = '';
} else {
row.style.display = 'none';
}
});
}
} catch (error) {
console.error('调用 API 时出错:', error);
}
};
const selectDevices = async () => {
await sleep(1000);
const cardIds = selectCardIds.split(',');
for (const cardId of cardIds) {
if (cardId.trim() !== '') {
let targetElement = document.querySelector("#" + cardId.trim() + " > div > div");
if (targetElement) {
targetElement.style.backgroundColor = backgroundColor === '' ? defaultColor : backgroundColor;
}
}
}
selectCardIds = '';
};
function autoFitContent() {
if(enableAutoFitContent && editor && editor.transformTool){
editor.transformTool.fitToBestPos();
}
}
function enhancedDisplayLog() {
if (!enableEnhancedDisplayLog) {
if (intervalId) {
clearInterval(intervalId);
}
} else {
intervalId = setInterval(() => {
var element = document.querySelector('.panel-log-card-blink');
if (element && element.style.outline !== "red solid 20px") {
element.style.outline = "red solid 10px";
}
let animateElement = document.querySelector('animate');
if (animateElement && animateElement.getAttribute('stroke-width') != '10') {
console
let pathElement = animateElement.parentElement;
pathElement.setAttribute('stroke-width', '10');
if (pathElement) {
let gElement = pathElement.parentElement;
gElement.setAttribute('stroke', 'red');
}
}
}, 500);
}
}
function sleep(ms) {
return new Promise(resolve => setTimeout(resolve, ms));
}
function isMiJiaJiKePage() {
return document.title === "米家自动化极客版" && !document.querySelector('.pin-form');
}
if(isMiJiaJiKePage()){
enhancedDisplayLog();
executeScript();
}
window.addEventListener('hashchange', () => {
if (isMiJiaJiKePage()) {
enhancedDisplayLog();
executeScript();
if (window.location.hash.match(/^#\/graph\/.*/g)) {
selectDevices();
setTimeout(function () {
autoFitContent();
}, 500);
}
}
});
window.addEventListener('click', () => {
if (window.location.hash.match(/^#\/graph\/.*/g)) {
setTimeout(function () {
autoFitContent();
}, 500);
}
});
})();