Browse Source

Home Assistant smart plug improvements:
- Add search parameter to /ha/entities endpoint for searching all entities
- Replace entity dropdown with searchable combobox (type to filter)
- Default shows switch/light/input_boolean; search queries all domains
- Make all three energy sensor dropdowns searchable (Power, Energy Today, Total)
- Each dropdown filters by entity_id or friendly_name
- Shows entity details (name, id, state, unit) in dropdown options

Fixes issue where HA plugs with non-standard entity naming couldn't
find their related power/energy sensors.

maziggy 4 months ago
parent
commit
4a3e42db36

+ 8 - 2
backend/app/api/routes/smart_plugs.py

@@ -211,9 +211,15 @@ async def test_ha_connection(request: HATestConnectionRequest):
 
 
 @router.get("/ha/entities", response_model=list[HAEntity])
-async def list_ha_entities(db: AsyncSession = Depends(get_db)):
+async def list_ha_entities(
+    db: AsyncSession = Depends(get_db),
+    search: str | None = None,
+):
     """List available Home Assistant entities.
 
+    By default, returns switch/light/input_boolean entities.
+    When search is provided, searches ALL entities by entity_id or friendly_name.
+
     Requires HA connection settings to be configured in Settings.
     """
     ha_url = await get_setting(db, "ha_url") or ""
@@ -224,7 +230,7 @@ async def list_ha_entities(db: AsyncSession = Depends(get_db)):
             400, "Home Assistant not configured. Please set HA URL and token in Settings → Network → Home Assistant."
         )
 
-    entities = await homeassistant_service.list_entities(ha_url, ha_token)
+    entities = await homeassistant_service.list_entities(ha_url, ha_token, search)
     return [HAEntity(**e) for e in entities]
 
 

+ 29 - 13
backend/app/services/homeassistant.py

@@ -215,8 +215,11 @@ class HomeAssistantService:
         except Exception as e:
             return {"success": False, "message": None, "error": str(e)}
 
-    async def list_entities(self, url: str, token: str) -> list[dict]:
-        """List available switch/light entities from HA.
+    async def list_entities(self, url: str, token: str, search: str | None = None) -> list[dict]:
+        """List available entities from HA.
+
+        By default, returns switch/light/input_boolean domains.
+        When search is provided, searches ALL entities by entity_id or friendly_name.
 
         Returns list of entity dicts with:
             - entity_id: str
@@ -224,6 +227,9 @@ class HomeAssistantService:
             - state: str
             - domain: str
         """
+        # Default domains for smart plug control
+        default_domains = {"switch", "light", "input_boolean"}
+
         try:
             async with httpx.AsyncClient(timeout=self.timeout) as client:
                 response = await client.get(
@@ -233,20 +239,30 @@ class HomeAssistantService:
                 response.raise_for_status()
 
                 entities = []
+                search_lower = search.lower().strip() if search else None
+
                 for entity in response.json():
                     entity_id = entity.get("entity_id", "")
                     domain = entity_id.split(".")[0] if "." in entity_id else ""
-
-                    # Filter to switch, light, input_boolean domains
-                    if domain in ["switch", "light", "input_boolean"]:
-                        entities.append(
-                            {
-                                "entity_id": entity_id,
-                                "friendly_name": entity.get("attributes", {}).get("friendly_name", entity_id),
-                                "state": entity.get("state"),
-                                "domain": domain,
-                            }
-                        )
+                    friendly_name = entity.get("attributes", {}).get("friendly_name", entity_id)
+
+                    # If searching, match against entity_id or friendly_name
+                    if search_lower:
+                        if search_lower not in entity_id.lower() and search_lower not in friendly_name.lower():
+                            continue
+                    else:
+                        # No search: filter to default domains only
+                        if domain not in default_domains:
+                            continue
+
+                    entities.append(
+                        {
+                            "entity_id": entity_id,
+                            "friendly_name": friendly_name,
+                            "state": entity.get("state"),
+                            "domain": domain,
+                        }
+                    )
 
                 return sorted(entities, key=lambda x: x["friendly_name"].lower())
         except Exception as e:

+ 4 - 2
frontend/src/api/client.ts

@@ -2211,8 +2211,10 @@ export const api = {
       method: 'POST',
       body: JSON.stringify({ url, token }),
     }),
-  getHAEntities: () =>
-    request<HAEntity[]>('/smart-plugs/ha/entities'),
+  getHAEntities: (search?: string) => {
+    const params = search ? `?search=${encodeURIComponent(search)}` : '';
+    return request<HAEntity[]>(`/smart-plugs/ha/entities${params}`);
+  },
   getHASensorEntities: () =>
     request<HASensorEntity[]>('/smart-plugs/ha/sensors'),
 

+ 384 - 95
frontend/src/components/AddSmartPlugModal.tsx

@@ -28,6 +28,24 @@ export function AddSmartPlugModal({ plug, onClose }: AddSmartPlugModalProps) {
   const [haPowerEntity, setHaPowerEntity] = useState(plug?.ha_power_entity || '');
   const [haEnergyTodayEntity, setHaEnergyTodayEntity] = useState(plug?.ha_energy_today_entity || '');
   const [haEnergyTotalEntity, setHaEnergyTotalEntity] = useState(plug?.ha_energy_total_entity || '');
+  // HA entity search
+  const [haEntitySearch, setHaEntitySearch] = useState('');
+  const [debouncedSearch, setDebouncedSearch] = useState('');
+  const [isEntityDropdownOpen, setIsEntityDropdownOpen] = useState(false);
+  const entityDropdownRef = useRef<HTMLDivElement>(null);
+
+  // Energy sensor search states
+  const [powerSensorSearch, setPowerSensorSearch] = useState('');
+  const [isPowerDropdownOpen, setIsPowerDropdownOpen] = useState(false);
+  const powerDropdownRef = useRef<HTMLDivElement>(null);
+
+  const [energyTodaySearch, setEnergyTodaySearch] = useState('');
+  const [isEnergyTodayDropdownOpen, setIsEnergyTodayDropdownOpen] = useState(false);
+  const energyTodayDropdownRef = useRef<HTMLDivElement>(null);
+
+  const [energyTotalSearch, setEnergyTotalSearch] = useState('');
+  const [isEnergyTotalDropdownOpen, setIsEnergyTotalDropdownOpen] = useState(false);
+  const energyTotalDropdownRef = useRef<HTMLDivElement>(null);
 
   const [printerId, setPrinterId] = useState<number | null>(plug?.printer_id || null);
   const [testResult, setTestResult] = useState<{ success: boolean; state?: string | null; device_name?: string | null } | null>(null);
@@ -73,10 +91,38 @@ export function AddSmartPlugModal({ plug, onClose }: AddSmartPlugModalProps) {
   // Check if HA is properly configured
   const haConfigured = !!(settings?.ha_enabled && settings?.ha_url && settings?.ha_token);
 
+  // Debounce search input
+  useEffect(() => {
+    const timer = setTimeout(() => {
+      setDebouncedSearch(haEntitySearch);
+    }, 300);
+    return () => clearTimeout(timer);
+  }, [haEntitySearch]);
+
+  // Close dropdowns when clicking outside
+  useEffect(() => {
+    const handleClickOutside = (e: MouseEvent) => {
+      if (entityDropdownRef.current && !entityDropdownRef.current.contains(e.target as Node)) {
+        setIsEntityDropdownOpen(false);
+      }
+      if (powerDropdownRef.current && !powerDropdownRef.current.contains(e.target as Node)) {
+        setIsPowerDropdownOpen(false);
+      }
+      if (energyTodayDropdownRef.current && !energyTodayDropdownRef.current.contains(e.target as Node)) {
+        setIsEnergyTodayDropdownOpen(false);
+      }
+      if (energyTotalDropdownRef.current && !energyTotalDropdownRef.current.contains(e.target as Node)) {
+        setIsEnergyTotalDropdownOpen(false);
+      }
+    };
+    document.addEventListener('mousedown', handleClickOutside);
+    return () => document.removeEventListener('mousedown', handleClickOutside);
+  }, []);
+
   // Fetch Home Assistant entities when in HA mode AND HA is configured
-  const { data: haEntities, isLoading: haEntitiesLoading } = useQuery({
-    queryKey: ['ha-entities'],
-    queryFn: api.getHAEntities,
+  const { data: haEntities, isLoading: haEntitiesLoading, error: haEntitiesError } = useQuery({
+    queryKey: ['ha-entities', debouncedSearch],
+    queryFn: () => api.getHAEntities(debouncedSearch || undefined),
     enabled: plugType === 'homeassistant' && haConfigured,
     retry: false,
     staleTime: 0,
@@ -439,61 +485,109 @@ export function AddSmartPlugModal({ plug, onClose }: AddSmartPlugModalProps) {
                     </div>
                   )}
 
-                  {haEntities && haEntities.length === 0 && (
-                    <div className="p-3 bg-yellow-500/20 border border-yellow-500/50 rounded-lg text-sm text-yellow-400">
-                      No switch/light entities found in Home Assistant
+                  {haEntitiesError && (
+                    <div className="p-3 bg-red-500/20 border border-red-500/50 rounded-lg text-sm text-red-400">
+                      Failed to load entities: {(haEntitiesError as Error).message}
                     </div>
                   )}
 
-                  {haEntities && haEntities.length > 0 && (() => {
+                  {/* Searchable Entity Dropdown */}
+                  {(() => {
                     // Filter out entities already configured (except current plug when editing)
                     const configuredEntityIds = existingPlugs
                       ?.filter(p => p.ha_entity_id && p.id !== plug?.id)
                       .map(p => p.ha_entity_id) || [];
-                    const availableEntities = haEntities.filter(e => !configuredEntityIds.includes(e.entity_id));
+                    const availableEntities = (haEntities || []).filter(e => !configuredEntityIds.includes(e.entity_id));
+                    const selectedEntity = haEntities?.find(e => e.entity_id === haEntityId);
 
                     return (
-                      <div>
+                      <div ref={entityDropdownRef} className="relative">
                         <label className="block text-sm text-bambu-gray mb-1">Select Entity *</label>
-                        <select
-                          value={haEntityId}
-                          onChange={(e) => {
-                            setHaEntityId(e.target.value);
-                            // Auto-fill name from entity friendly name
-                            const entity = haEntities?.find(ent => ent.entity_id === e.target.value);
-                            if (entity && !name) {
-                              setName(entity.friendly_name);
-                            }
-                          }}
-                          className="w-full px-3 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white focus:border-bambu-green focus:outline-none"
-                        >
-                          <option value="">Choose an entity...</option>
-                          {availableEntities.map((entity) => (
-                            <option key={entity.entity_id} value={entity.entity_id}>
-                              {entity.friendly_name} ({entity.entity_id}) - {entity.state}
-                            </option>
-                          ))}
-                        </select>
-                        {configuredEntityIds.length > 0 && (
-                          <p className="text-xs text-bambu-gray mt-1">
-                            {configuredEntityIds.length} entity(s) already configured
-                          </p>
+                        <div className="relative">
+                          <Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-bambu-gray pointer-events-none" />
+                          <input
+                            type="text"
+                            value={isEntityDropdownOpen ? haEntitySearch : (selectedEntity ? `${selectedEntity.friendly_name} (${selectedEntity.entity_id})` : '')}
+                            onChange={(e) => {
+                              setHaEntitySearch(e.target.value);
+                              if (!isEntityDropdownOpen) setIsEntityDropdownOpen(true);
+                            }}
+                            onFocus={() => {
+                              setIsEntityDropdownOpen(true);
+                              setHaEntitySearch('');
+                            }}
+                            placeholder="Search entities..."
+                            className="w-full pl-9 pr-8 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white placeholder-bambu-gray focus:border-bambu-green focus:outline-none"
+                          />
+                          {haEntityId && !isEntityDropdownOpen && (
+                            <button
+                              type="button"
+                              onClick={() => {
+                                setHaEntityId('');
+                                setHaEntitySearch('');
+                              }}
+                              className="absolute right-2 top-1/2 -translate-y-1/2 p-1 hover:bg-bambu-dark-tertiary rounded"
+                            >
+                              <X className="w-4 h-4 text-bambu-gray hover:text-white" />
+                            </button>
+                          )}
+                          {haEntitiesLoading && (
+                            <Loader2 className="absolute right-2 top-1/2 -translate-y-1/2 w-4 h-4 text-bambu-gray animate-spin" />
+                          )}
+                        </div>
+
+                        {/* Dropdown */}
+                        {isEntityDropdownOpen && (
+                          <div className="absolute z-50 w-full mt-1 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg shadow-lg max-h-60 overflow-y-auto">
+                            {haEntitiesLoading && (
+                              <div className="px-3 py-2 text-sm text-bambu-gray flex items-center gap-2">
+                                <Loader2 className="w-4 h-4 animate-spin" />
+                                Loading...
+                              </div>
+                            )}
+                            {!haEntitiesLoading && availableEntities.length === 0 && (
+                              <div className="px-3 py-2 text-sm text-bambu-gray">
+                                {debouncedSearch
+                                  ? `No entities found matching "${debouncedSearch}"`
+                                  : 'No entities available'}
+                              </div>
+                            )}
+                            {!haEntitiesLoading && availableEntities.map((entity) => (
+                              <button
+                                key={entity.entity_id}
+                                type="button"
+                                onClick={() => {
+                                  setHaEntityId(entity.entity_id);
+                                  setIsEntityDropdownOpen(false);
+                                  setHaEntitySearch('');
+                                  // Auto-fill name
+                                  if (!name) {
+                                    setName(entity.friendly_name);
+                                  }
+                                }}
+                                className={`w-full px-3 py-2 text-left text-sm hover:bg-bambu-dark-tertiary transition-colors ${
+                                  entity.entity_id === haEntityId ? 'bg-bambu-green/20 text-bambu-green' : 'text-white'
+                                }`}
+                              >
+                                <div className="font-medium">{entity.friendly_name}</div>
+                                <div className="text-xs text-bambu-gray flex items-center justify-between">
+                                  <span>{entity.entity_id}</span>
+                                  <span className={entity.state === 'on' ? 'text-bambu-green' : ''}>{entity.state}</span>
+                                </div>
+                              </button>
+                            ))}
+                          </div>
                         )}
+
+                        <p className="text-xs text-bambu-gray mt-1">
+                          {debouncedSearch
+                            ? `Searching all entities (${availableEntities.length} found)`
+                            : `Showing switch, light, input_boolean (${availableEntities.length} available)`}
+                        </p>
                       </div>
                     );
                   })()}
 
-                  {haEntityId && haEntities && (
-                    <div className="p-3 bg-bambu-green/20 border border-bambu-green/50 rounded-lg text-sm text-bambu-green flex items-center gap-2">
-                      <CheckCircle className="w-5 h-5" />
-                      <div>
-                        <p className="font-medium">Entity selected</p>
-                        <p className="text-xs opacity-80">
-                          {haEntities.find(e => e.entity_id === haEntityId)?.friendly_name} - {haEntities.find(e => e.entity_id === haEntityId)?.state}
-                        </p>
-                      </div>
-                    </div>
-                  )}
 
                   {/* Energy Monitoring Section (Optional) */}
                   {haEntityId && haSensorEntities && haSensorEntities.length > 0 && (
@@ -501,66 +595,261 @@ export function AddSmartPlugModal({ plug, onClose }: AddSmartPlugModalProps) {
                       <div>
                         <p className="text-white font-medium mb-1">Energy Monitoring (Optional)</p>
                         <p className="text-xs text-bambu-gray mb-3">
-                          Select sensors that provide power/energy data. These can be from any device in Home Assistant.
+                          Search and select sensors that provide power/energy data.
                         </p>
                       </div>
 
                       {/* Power Sensor (W) */}
-                      <div>
-                        <label className="block text-sm text-bambu-gray mb-1">Power Sensor (W)</label>
-                        <select
-                          value={haPowerEntity}
-                          onChange={(e) => setHaPowerEntity(e.target.value)}
-                          className="w-full px-3 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white focus:border-bambu-green focus:outline-none"
-                        >
-                          <option value="">None</option>
-                          {haSensorEntities
-                            .filter(s => s.unit_of_measurement === 'W' || s.unit_of_measurement === 'kW' || s.unit_of_measurement === 'mW')
-                            .map((sensor) => (
-                              <option key={sensor.entity_id} value={sensor.entity_id}>
-                                {sensor.friendly_name} ({sensor.state} {sensor.unit_of_measurement})
-                              </option>
-                            ))}
-                        </select>
-                      </div>
+                      {(() => {
+                        const powerSensors = haSensorEntities.filter(s =>
+                          s.unit_of_measurement === 'W' || s.unit_of_measurement === 'kW' || s.unit_of_measurement === 'mW'
+                        );
+                        const filteredPowerSensors = powerSensorSearch
+                          ? powerSensors.filter(s =>
+                              s.entity_id.toLowerCase().includes(powerSensorSearch.toLowerCase()) ||
+                              s.friendly_name.toLowerCase().includes(powerSensorSearch.toLowerCase())
+                            )
+                          : powerSensors;
+                        const selectedPowerSensor = haSensorEntities.find(s => s.entity_id === haPowerEntity);
+
+                        return (
+                          <div ref={powerDropdownRef} className="relative">
+                            <label className="block text-sm text-bambu-gray mb-1">Power Sensor (W)</label>
+                            <div className="relative">
+                              <Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-bambu-gray pointer-events-none" />
+                              <input
+                                type="text"
+                                value={isPowerDropdownOpen ? powerSensorSearch : (selectedPowerSensor ? `${selectedPowerSensor.friendly_name} (${selectedPowerSensor.state} ${selectedPowerSensor.unit_of_measurement})` : '')}
+                                onChange={(e) => {
+                                  setPowerSensorSearch(e.target.value);
+                                  if (!isPowerDropdownOpen) setIsPowerDropdownOpen(true);
+                                }}
+                                onFocus={() => {
+                                  setIsPowerDropdownOpen(true);
+                                  setPowerSensorSearch('');
+                                }}
+                                placeholder="Search power sensors..."
+                                className="w-full pl-9 pr-8 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white placeholder-bambu-gray focus:border-bambu-green focus:outline-none"
+                              />
+                              {haPowerEntity && !isPowerDropdownOpen && (
+                                <button
+                                  type="button"
+                                  onClick={() => {
+                                    setHaPowerEntity('');
+                                    setPowerSensorSearch('');
+                                  }}
+                                  className="absolute right-2 top-1/2 -translate-y-1/2 p-1 hover:bg-bambu-dark-tertiary rounded"
+                                >
+                                  <X className="w-4 h-4 text-bambu-gray hover:text-white" />
+                                </button>
+                              )}
+                            </div>
+                            {isPowerDropdownOpen && (
+                              <div className="absolute z-50 w-full mt-1 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg shadow-lg max-h-48 overflow-y-auto">
+                                <button
+                                  type="button"
+                                  onClick={() => {
+                                    setHaPowerEntity('');
+                                    setIsPowerDropdownOpen(false);
+                                    setPowerSensorSearch('');
+                                  }}
+                                  className="w-full px-3 py-2 text-left text-sm text-bambu-gray hover:bg-bambu-dark-tertiary"
+                                >
+                                  None
+                                </button>
+                                {filteredPowerSensors.map((sensor) => (
+                                  <button
+                                    key={sensor.entity_id}
+                                    type="button"
+                                    onClick={() => {
+                                      setHaPowerEntity(sensor.entity_id);
+                                      setIsPowerDropdownOpen(false);
+                                      setPowerSensorSearch('');
+                                    }}
+                                    className={`w-full px-3 py-2 text-left text-sm hover:bg-bambu-dark-tertiary ${
+                                      sensor.entity_id === haPowerEntity ? 'bg-bambu-green/20 text-bambu-green' : 'text-white'
+                                    }`}
+                                  >
+                                    <div className="font-medium">{sensor.friendly_name}</div>
+                                    <div className="text-xs text-bambu-gray">{sensor.entity_id} • {sensor.state} {sensor.unit_of_measurement}</div>
+                                  </button>
+                                ))}
+                                {filteredPowerSensors.length === 0 && (
+                                  <div className="px-3 py-2 text-sm text-bambu-gray">No matching sensors</div>
+                                )}
+                              </div>
+                            )}
+                          </div>
+                        );
+                      })()}
 
                       {/* Energy Today (kWh) */}
-                      <div>
-                        <label className="block text-sm text-bambu-gray mb-1">Energy Today (kWh)</label>
-                        <select
-                          value={haEnergyTodayEntity}
-                          onChange={(e) => setHaEnergyTodayEntity(e.target.value)}
-                          className="w-full px-3 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white focus:border-bambu-green focus:outline-none"
-                        >
-                          <option value="">None</option>
-                          {haSensorEntities
-                            .filter(s => s.unit_of_measurement === 'kWh' || s.unit_of_measurement === 'Wh' || s.unit_of_measurement === 'MWh')
-                            .map((sensor) => (
-                              <option key={sensor.entity_id} value={sensor.entity_id}>
-                                {sensor.friendly_name} ({sensor.state} {sensor.unit_of_measurement})
-                              </option>
-                            ))}
-                        </select>
-                      </div>
+                      {(() => {
+                        const energySensors = haSensorEntities.filter(s =>
+                          s.unit_of_measurement === 'kWh' || s.unit_of_measurement === 'Wh' || s.unit_of_measurement === 'MWh'
+                        );
+                        const filteredEnergySensors = energyTodaySearch
+                          ? energySensors.filter(s =>
+                              s.entity_id.toLowerCase().includes(energyTodaySearch.toLowerCase()) ||
+                              s.friendly_name.toLowerCase().includes(energyTodaySearch.toLowerCase())
+                            )
+                          : energySensors;
+                        const selectedSensor = haSensorEntities.find(s => s.entity_id === haEnergyTodayEntity);
+
+                        return (
+                          <div ref={energyTodayDropdownRef} className="relative">
+                            <label className="block text-sm text-bambu-gray mb-1">Energy Today (kWh)</label>
+                            <div className="relative">
+                              <Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-bambu-gray pointer-events-none" />
+                              <input
+                                type="text"
+                                value={isEnergyTodayDropdownOpen ? energyTodaySearch : (selectedSensor ? `${selectedSensor.friendly_name} (${selectedSensor.state} ${selectedSensor.unit_of_measurement})` : '')}
+                                onChange={(e) => {
+                                  setEnergyTodaySearch(e.target.value);
+                                  if (!isEnergyTodayDropdownOpen) setIsEnergyTodayDropdownOpen(true);
+                                }}
+                                onFocus={() => {
+                                  setIsEnergyTodayDropdownOpen(true);
+                                  setEnergyTodaySearch('');
+                                }}
+                                placeholder="Search energy sensors..."
+                                className="w-full pl-9 pr-8 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white placeholder-bambu-gray focus:border-bambu-green focus:outline-none"
+                              />
+                              {haEnergyTodayEntity && !isEnergyTodayDropdownOpen && (
+                                <button
+                                  type="button"
+                                  onClick={() => {
+                                    setHaEnergyTodayEntity('');
+                                    setEnergyTodaySearch('');
+                                  }}
+                                  className="absolute right-2 top-1/2 -translate-y-1/2 p-1 hover:bg-bambu-dark-tertiary rounded"
+                                >
+                                  <X className="w-4 h-4 text-bambu-gray hover:text-white" />
+                                </button>
+                              )}
+                            </div>
+                            {isEnergyTodayDropdownOpen && (
+                              <div className="absolute z-50 w-full mt-1 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg shadow-lg max-h-48 overflow-y-auto">
+                                <button
+                                  type="button"
+                                  onClick={() => {
+                                    setHaEnergyTodayEntity('');
+                                    setIsEnergyTodayDropdownOpen(false);
+                                    setEnergyTodaySearch('');
+                                  }}
+                                  className="w-full px-3 py-2 text-left text-sm text-bambu-gray hover:bg-bambu-dark-tertiary"
+                                >
+                                  None
+                                </button>
+                                {filteredEnergySensors.map((sensor) => (
+                                  <button
+                                    key={sensor.entity_id}
+                                    type="button"
+                                    onClick={() => {
+                                      setHaEnergyTodayEntity(sensor.entity_id);
+                                      setIsEnergyTodayDropdownOpen(false);
+                                      setEnergyTodaySearch('');
+                                    }}
+                                    className={`w-full px-3 py-2 text-left text-sm hover:bg-bambu-dark-tertiary ${
+                                      sensor.entity_id === haEnergyTodayEntity ? 'bg-bambu-green/20 text-bambu-green' : 'text-white'
+                                    }`}
+                                  >
+                                    <div className="font-medium">{sensor.friendly_name}</div>
+                                    <div className="text-xs text-bambu-gray">{sensor.entity_id} • {sensor.state} {sensor.unit_of_measurement}</div>
+                                  </button>
+                                ))}
+                                {filteredEnergySensors.length === 0 && (
+                                  <div className="px-3 py-2 text-sm text-bambu-gray">No matching sensors</div>
+                                )}
+                              </div>
+                            )}
+                          </div>
+                        );
+                      })()}
 
                       {/* Total Energy (kWh) */}
-                      <div>
-                        <label className="block text-sm text-bambu-gray mb-1">Total Energy (kWh)</label>
-                        <select
-                          value={haEnergyTotalEntity}
-                          onChange={(e) => setHaEnergyTotalEntity(e.target.value)}
-                          className="w-full px-3 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white focus:border-bambu-green focus:outline-none"
-                        >
-                          <option value="">None</option>
-                          {haSensorEntities
-                            .filter(s => s.unit_of_measurement === 'kWh' || s.unit_of_measurement === 'Wh' || s.unit_of_measurement === 'MWh')
-                            .map((sensor) => (
-                              <option key={sensor.entity_id} value={sensor.entity_id}>
-                                {sensor.friendly_name} ({sensor.state} {sensor.unit_of_measurement})
-                              </option>
-                            ))}
-                        </select>
-                      </div>
+                      {(() => {
+                        const energySensors = haSensorEntities.filter(s =>
+                          s.unit_of_measurement === 'kWh' || s.unit_of_measurement === 'Wh' || s.unit_of_measurement === 'MWh'
+                        );
+                        const filteredEnergySensors = energyTotalSearch
+                          ? energySensors.filter(s =>
+                              s.entity_id.toLowerCase().includes(energyTotalSearch.toLowerCase()) ||
+                              s.friendly_name.toLowerCase().includes(energyTotalSearch.toLowerCase())
+                            )
+                          : energySensors;
+                        const selectedSensor = haSensorEntities.find(s => s.entity_id === haEnergyTotalEntity);
+
+                        return (
+                          <div ref={energyTotalDropdownRef} className="relative">
+                            <label className="block text-sm text-bambu-gray mb-1">Total Energy (kWh)</label>
+                            <div className="relative">
+                              <Search className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-bambu-gray pointer-events-none" />
+                              <input
+                                type="text"
+                                value={isEnergyTotalDropdownOpen ? energyTotalSearch : (selectedSensor ? `${selectedSensor.friendly_name} (${selectedSensor.state} ${selectedSensor.unit_of_measurement})` : '')}
+                                onChange={(e) => {
+                                  setEnergyTotalSearch(e.target.value);
+                                  if (!isEnergyTotalDropdownOpen) setIsEnergyTotalDropdownOpen(true);
+                                }}
+                                onFocus={() => {
+                                  setIsEnergyTotalDropdownOpen(true);
+                                  setEnergyTotalSearch('');
+                                }}
+                                placeholder="Search energy sensors..."
+                                className="w-full pl-9 pr-8 py-2 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg text-white placeholder-bambu-gray focus:border-bambu-green focus:outline-none"
+                              />
+                              {haEnergyTotalEntity && !isEnergyTotalDropdownOpen && (
+                                <button
+                                  type="button"
+                                  onClick={() => {
+                                    setHaEnergyTotalEntity('');
+                                    setEnergyTotalSearch('');
+                                  }}
+                                  className="absolute right-2 top-1/2 -translate-y-1/2 p-1 hover:bg-bambu-dark-tertiary rounded"
+                                >
+                                  <X className="w-4 h-4 text-bambu-gray hover:text-white" />
+                                </button>
+                              )}
+                            </div>
+                            {isEnergyTotalDropdownOpen && (
+                              <div className="absolute z-50 w-full mt-1 bg-bambu-dark border border-bambu-dark-tertiary rounded-lg shadow-lg max-h-48 overflow-y-auto">
+                                <button
+                                  type="button"
+                                  onClick={() => {
+                                    setHaEnergyTotalEntity('');
+                                    setIsEnergyTotalDropdownOpen(false);
+                                    setEnergyTotalSearch('');
+                                  }}
+                                  className="w-full px-3 py-2 text-left text-sm text-bambu-gray hover:bg-bambu-dark-tertiary"
+                                >
+                                  None
+                                </button>
+                                {filteredEnergySensors.map((sensor) => (
+                                  <button
+                                    key={sensor.entity_id}
+                                    type="button"
+                                    onClick={() => {
+                                      setHaEnergyTotalEntity(sensor.entity_id);
+                                      setIsEnergyTotalDropdownOpen(false);
+                                      setEnergyTotalSearch('');
+                                    }}
+                                    className={`w-full px-3 py-2 text-left text-sm hover:bg-bambu-dark-tertiary ${
+                                      sensor.entity_id === haEnergyTotalEntity ? 'bg-bambu-green/20 text-bambu-green' : 'text-white'
+                                    }`}
+                                  >
+                                    <div className="font-medium">{sensor.friendly_name}</div>
+                                    <div className="text-xs text-bambu-gray">{sensor.entity_id} • {sensor.state} {sensor.unit_of_measurement}</div>
+                                  </button>
+                                ))}
+                                {filteredEnergySensors.length === 0 && (
+                                  <div className="px-3 py-2 text-sm text-bambu-gray">No matching sensors</div>
+                                )}
+                              </div>
+                            )}
+                          </div>
+                        );
+                      })()}
                     </div>
                   )}
                 </>

File diff suppressed because it is too large
+ 0 - 0
static/assets/index-XGGhfA3J.js


+ 1 - 1
static/index.html

@@ -23,7 +23,7 @@
 
     <!-- Splash screens for iOS -->
     <link rel="apple-touch-startup-image" href="/img/android-chrome-512x512.png" />
-    <script type="module" crossorigin src="/assets/index-DbTLv2ta.js"></script>
+    <script type="module" crossorigin src="/assets/index-XGGhfA3J.js"></script>
     <link rel="stylesheet" crossorigin href="/assets/index-C_p2QVEb.css">
   </head>
   <body>

Some files were not shown because too many files changed in this diff