米家极客版油猴插件

在极客页面中,点击设备列表,调用API获取设备和规则列表,并生成设备规则映射并显示在当前页面上

当前为 2024-05-23 提交的版本,查看 最新版本

// ==UserScript==
// @name         米家极客版油猴插件
// @namespace    http://tampermonkey.net/
// @version      v0.8.4
// @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=800;
    let defaultWindowHeight=800;
    let enableEnhancedDisplayLog=GM_getValue("enableEnhancedDisplayLog");
    let backgroundColor = GM_getValue("backgroundColor") ;
    let windowWidth = GM_getValue("windowWidth");
    let windowHeight = GM_getValue("windowHeight");

    if (enableEnhancedDisplayLog === undefined || enableEnhancedDisplayLog === null || enableEnhancedDisplayLog === "") {
        enableEnhancedDisplayLog = 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 label = document.createElement('label');
            label.htmlFor = 'highlightLogCheck';
            label.appendChild(document.createTextNode('日志高亮'));
            label.style.marginBottom = '10px';
            label.style.marginLeft = '10px';
            container.appendChild(label);
            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 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 sleep(ms) {
            return new Promise(resolve => setTimeout(resolve, ms));
        }
    };

    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 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();
            }
        }
    });
})();