12.4 KiB · text History 04ed4f9
import QtQuick
import QtQuick.Controls as QQC2
import QtQuick.Layouts
import org.kde.kirigami as Kirigami

QQC2.ComboBox {
    id: entityBrowser
    
    property alias homeAssistantAPI: homeAssistantAPI
    property string selectedEntityId: ""
    property string selectedEntityName: ""
    property var selectedEntity: null
    property var filteredDomains: [] // Empty = all domains, or ["light", "switch"] etc.
    property string searchQuery: ""
    // Set while editText is changed programmatically so it doesn't trigger a re-filter
    property bool suppressFilter: false
    
    signal entitySelected(var entity)
    
    editable: true
    textRole: "display_text"
    valueRole: "entity_id"
    
    // Custom model for entities
    model: ListModel {
        id: entityModel
    }
    
    onEditTextChanged: {
        if (suppressFilter) {
            return
        }
        searchQuery = editText
        // Defer so the model is never rebuilt from inside a ComboBox/delegate signal handler
        filterTimer.restart()
    }
    
    onActivated: function(index) {
        if (index >= 0 && index < entityModel.count) {
            selectEntity(entityModel.get(index))
        }
    }
    
    Timer {
        id: filterTimer
        interval: 150
        onTriggered: filterEntities()
    }
    
    function setEditTextSilently(text) {
        suppressFilter = true
        editText = text
        suppressFilter = false
    }
    
    function clearSelection() {
        setEditTextSilently("")
        searchQuery = ""
        selectedEntityId = ""
        selectedEntityName = ""
        selectedEntity = null
    }
    
    function selectEntity(item) {
        // Copy into a plain object: model/ListModel proxies become invalid once the model is rebuilt
        var entity = {
            entity_id: item.entity_id,
            friendly_name: item.friendly_name,
            domain: item.domain,
            state: item.state,
            icon: item.icon,
            device_class: item.device_class,
            unit_of_measurement: item.unit_of_measurement,
            display_text: item.display_text
        }
        selectedEntityId = entity.entity_id
        selectedEntityName = entity.friendly_name
        selectedEntity = entity
        setEditTextSilently(entity.display_text)
        entitySelected(entity)
    }
    
    // Custom popup for better entity browsing
    popup: QQC2.Popup {
        id: entityPopup
        y: entityBrowser.height
        width: Math.max(entityBrowser.width, Kirigami.Units.gridUnit * 20)
        height: Math.min(contentItem.implicitHeight, Kirigami.Units.gridUnit * 15)
        
        contentItem: ColumnLayout {
            spacing: Kirigami.Units.smallSpacing
            
            // Domain filter
            RowLayout {
                Layout.fillWidth: true
                
                QQC2.Label {
                    text: i18n("Filter:")
                    font.pointSize: Kirigami.Theme.smallFont.pointSize
                }
                
                Repeater {
                    model: ["all", "light", "switch", "fan", "automation", "climate", "cover", "lock"]
                    
                    QQC2.Button {
                        text: modelData === "all" ? i18n("All") : modelData
                        checkable: true
                        checked: filteredDomains.length === 0 && modelData === "all" || 
                                filteredDomains.indexOf(modelData) !== -1
                        
                        onClicked: {
                            if (modelData === "all") {
                                filteredDomains = []
                            } else {
                                var domains = filteredDomains.slice() // copy array
                                var index = domains.indexOf(modelData)
                                if (index !== -1) {
                                    domains.splice(index, 1)
                                } else {
                                    domains.push(modelData)
                                }
                                filteredDomains = domains
                            }
                            filterEntities()
                        }
                        
                        font.pointSize: Kirigami.Theme.smallFont.pointSize
                        Layout.preferredHeight: Kirigami.Units.gridUnit * 1.2
                    }
                }
            }
            
            Kirigami.Separator {
                Layout.fillWidth: true
            }
            
            // Entity list
            QQC2.ScrollView {
                Layout.fillWidth: true
                Layout.fillHeight: true
                
                ListView {
                    id: entityListView
                    model: entityModel
                    
                    delegate: QQC2.ItemDelegate {
                        width: entityListView.width
                        
                        contentItem: RowLayout {
                            spacing: Kirigami.Units.smallSpacing
                            
                            // Domain badge
                            Rectangle {
                                Layout.preferredWidth: Kirigami.Units.gridUnit * 2
                                Layout.preferredHeight: Kirigami.Units.gridUnit * 0.8
                                radius: Kirigami.Units.cornerRadius
                                color: getDomainColor(model.domain)
                                
                                QQC2.Label {
                                    anchors.centerIn: parent
                                    text: model.domain
                                    color: "white"
                                    font.pointSize: Kirigami.Theme.smallFont.pointSize
                                    font.bold: true
                                }
                            }
                            
                            // Entity icon
                            Kirigami.Icon {
                                source: getEntityIcon(model)
                                Layout.preferredWidth: Kirigami.Units.iconSizes.small
                                Layout.preferredHeight: Kirigami.Units.iconSizes.small
                            }
                            
                            // Entity info
                            ColumnLayout {
                                Layout.fillWidth: true
                                spacing: 0
                                
                                QQC2.Label {
                                    text: model.friendly_name
                                    font.bold: true
                                    Layout.fillWidth: true
                                    elide: Text.ElideRight
                                }
                                
                                QQC2.Label {
                                    text: model.entity_id + " • " + model.state
                                    color: Kirigami.Theme.disabledTextColor
                                    font.pointSize: Kirigami.Theme.smallFont.pointSize
                                    Layout.fillWidth: true
                                    elide: Text.ElideRight
                                }
                            }
                            
                            // State indicator
                            Rectangle {
                                Layout.preferredWidth: Kirigami.Units.gridUnit * 0.5
                                Layout.preferredHeight: Kirigami.Units.gridUnit * 0.5
                                radius: width / 2
                                color: getStateColor(model.state)
                            }
                        }
                        
                        onClicked: {
                            entityPopup.close()
                            selectEntity(model)
                        }
                    }
                }
            }
            
            // Status bar
            RowLayout {
                Layout.fillWidth: true
                
                QQC2.Label {
                    text: i18n("%1 entities found", entityModel.count)
                    font.pointSize: Kirigami.Theme.smallFont.pointSize
                    color: Kirigami.Theme.disabledTextColor
                }
                
                Item { Layout.fillWidth: true }
                
                QQC2.BusyIndicator {
                    visible: homeAssistantAPI.isLoadingEntities
                    Layout.preferredWidth: Kirigami.Units.iconSizes.small
                    Layout.preferredHeight: Kirigami.Units.iconSizes.small
                }
            }
        }
    }
    
    // Home Assistant API instance
    HomeAssistantAPI {
        id: homeAssistantAPI
        
        onEntitiesLoaded: function(entities) {
            filterEntities()
        }
        
        onError: function(message) {
            console.log("Entity browser API error:", message)
        }
    }
    
    function loadEntities() {
        if (homeAssistantAPI.baseUrl && homeAssistantAPI.accessToken) {
            homeAssistantAPI.getAllEntities()
        }
    }
    
    function filterEntities() {
        // Rebuilding the model makes the editable ComboBox reset editText, which would
        // wipe the user's query and re-trigger filtering; keep the text and suppress that.
        var typedText = editText
        suppressFilter = true
        rebuildModel()
        editText = typedText
        suppressFilter = false
    }
    
    function rebuildModel() {
        entityModel.clear()
        
        if (!homeAssistantAPI.allEntities || homeAssistantAPI.allEntities.length === 0) {
            return
        }
        
        var searchResults = homeAssistantAPI.searchEntities(searchQuery, filteredDomains)
        
        // Limit results to prevent UI lag
        var maxResults = 100
        for (var i = 0; i < Math.min(searchResults.length, maxResults); i++) {
            var entity = searchResults[i]
            entityModel.append({
                entity_id: entity.entity_id,
                friendly_name: entity.friendly_name,
                domain: entity.domain,
                state: entity.state,
                icon: entity.icon,
                device_class: entity.device_class,
                unit_of_measurement: entity.unit_of_measurement,
                display_text: entity.friendly_name + " (" + entity.entity_id + ")"
            })
        }
    }
    
    function getDomainColor(domain) {
        switch (domain) {
            case "light": return "#FFA726"
            case "switch": return "#42A5F5"
            case "fan": return "#66BB6A"
            case "automation": return "#AB47BC"
            case "climate": return "#EF5350"
            case "cover": return "#26A69A"
            case "lock": return "#8D6E63"
            case "sensor": return "#78909C"
            case "binary_sensor": return "#90A4AE"
            default: return "#9E9E9E"
        }
    }
    
    function getEntityIcon(entity) {
        if (entity.icon) {
            return entity.icon.replace("mdi:", "")
        }
        
        switch (entity.domain) {
            case "light": return entity.state === "on" ? "lightbulb-on" : "lightbulb"
            case "switch": return entity.state === "on" ? "toggle-switch" : "toggle-switch-off"
            case "fan": return "fan"
            case "automation": return "playlist-play"
            case "climate": return "thermometer"
            case "cover": return "window-close"
            case "lock": return entity.state === "locked" ? "lock" : "lock-open"
            case "sensor": return "gauge"
            case "binary_sensor": return "checkbox-marked-circle"
            default: return "home-assistant"
        }
    }
    
    function getStateColor(state) {
        switch (state) {
            case "on":
            case "open":
            case "home":
            case "unlocked":
                return Kirigami.Theme.positiveTextColor
            case "off":
            case "closed":
            case "away":
            case "locked":
                return Kirigami.Theme.disabledTextColor
            case "unavailable":
            case "unknown":
                return Kirigami.Theme.negativeTextColor
            default:
                return Kirigami.Theme.textColor
        }
    }
    
    Component.onCompleted: {
        // Auto-load entities when component is ready
        Qt.callLater(loadEntities)
    }
}