Greasy Fork

Greasy Fork is available in English.

米家极客版油猴插件

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

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

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Greasemonkey 油猴子Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Violentmonkey 暴力猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴Userscripts ,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展,例如 Tampermonkey 篡改猴,才能安装此脚本。

您需要先安装一款用户脚本管理器扩展后才能安装此脚本。

(我已经安装了用户脚本管理器,让我安装!)

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展,比如 Stylus,才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

您需要先安装一款用户样式管理器扩展后才能安装此样式。

(我已经安装了用户样式管理器,让我安装!)

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