米家极客版油猴插件

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

目前為 2024-05-22 提交的版本,檢視 最新版本

// ==UserScript==
// @name         米家极客版油猴插件
// @namespace    http://tampermonkey.net/
// @version      v0.8
// @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 selectCardIds = '';
    let defaultColor='#43ad7f7f'
    let backgroundColor = GM_getValue("backgroundColor") ?? '';
    let windowWidth = GM_getValue("windowWidth") ?? 800;
    let windowHeight= GM_getValue("windowHeight") ?? 600;


    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 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(d => d !== undefined));
                const cards = new Set(content.nodes.map(n => {
                    return (n.props && n.cfg) ? {did: n.props.did, oriId: n.cfg.oriId} : undefined;
                }).filter(d => d !== undefined));

                dids.forEach(d => {
                    devRuleMap[d] = devRuleMap[d] ?? [];
                    const cardIds = Array.from(cards)
                        .filter(card => card.did === d)
                        .map(card => card.oriId).join(',');
                    const tempDevRule = {
                        ruleId: rule.id,
                        cardIds: cardIds,
                        totalCardNum: cards.size
                    };

                    devRuleMap[d].push(tempDevRule);
                });
            }

            const result = Object.fromEntries(Object.entries(devRuleMap).map(([k, v]) => [
                devList[k]?.name ?? `did: ${k}`,
                v.map(r => {
                    return ({
                        id: r.ruleId,
                        cardIds: r.cardIds,
                        totalCardNum: r.totalCardNum,
                        name: ruleList.find(rr => rr.id === r.ruleId).userData.name
                    });
                })
            ]));

            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 = '50px';
                    container.style.width = '50px';
                    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 deviceHeader = document.createElement('th');
            const ruleHeader = document.createElement('th');

            let deviceSortOrder = 'asc';
            let ruleSortOrder = 'asc';

            const updateSortMarkers = () => {
                deviceHeader.innerHTML = `设备 ${deviceSortOrder === 'asc' ? '⬆️' : '⬇️'}`;
                ruleHeader.innerHTML = `规则 ${ruleSortOrder === 'asc' ? '⬆️' : '⬇️'}`;
            };

            deviceHeader.textContent = '设备';
            ruleHeader.textContent = '规则';

            deviceHeader.onclick = () => {
                deviceSortOrder = deviceSortOrder === 'asc' ? 'desc' : 'asc';
                sortTable(0, deviceSortOrder);
                updateSortMarkers();
            };
            ruleHeader.onclick = () => {
                ruleSortOrder = ruleSortOrder === 'asc' ? 'desc' : 'asc';
                sortTable(1, ruleSortOrder);
                updateSortMarkers();
            };

            headerRow.appendChild(deviceHeader);
            headerRow.appendChild(ruleHeader);
            thead.appendChild(headerRow);
            table.appendChild(thead);

            const deviceFilterInput = document.createElement('input');
            deviceFilterInput.type = 'text';
            deviceFilterInput.placeholder = '设备筛选';
            deviceFilterInput.style.marginBottom = '10px';
            deviceFilterInput.oninput = () => {
                filterTable(deviceFilterInput.value, ruleFilterInput.value);
            };
            container.appendChild(deviceFilterInput);

            const ruleFilterInput = document.createElement('input');
            ruleFilterInput.type = 'text';
            ruleFilterInput.placeholder = '规则筛选';
            ruleFilterInput.style.marginBottom = '10px';
            ruleFilterInput.style.marginLeft = '10px';
            ruleFilterInput.oninput = () => {
                filterTable(deviceFilterInput.value, ruleFilterInput.value);
            };
            container.appendChild(ruleFilterInput);

            const widthInput = document.createElement('input');
            widthInput.type = 'text';
            widthInput.placeholder = windowWidth+'px';
            widthInput.style.width = '80px';
            widthInput.style.marginBottom = '10px';
            widthInput.style.marginLeft = '10px';
            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 = '80px';
            heightInput.style.marginBottom = '10px';
            heightInput.style.marginLeft = '10px';
            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.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 tbody = document.createElement('tbody');
            Object.entries(result).forEach(([device, rules]) => {
                const row = document.createElement('tr');
                const deviceCell = document.createElement('td');
                deviceCell.textContent = device;
                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(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(deviceKeyword, ruleKeyword) {
                const rows = Array.from(tbody.rows);
                rows.forEach(row => {
                    const deviceText = row.cells[0].textContent.toLowerCase();
                    const ruleText = row.cells[1].textContent.toLowerCase();
                    if (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 isMiJiaJiKePage() {
        return document.title === "米家自动化极客版";
    }

    if (isMiJiaJiKePage() && window.location.hash === '#/device') {
        executeScript();
    }

    window.addEventListener('hashchange', () => {
        if (isMiJiaJiKePage() && window.location.hash === '#/device') {
            executeScript();
        }
        if (isMiJiaJiKePage() && window.location.hash.match(/^#\/graph\/.*/g)) {
            selectDevices();
        }
    });
})();