From 7167f8ea0edaa5c29294c9501a00c92599bf6764 Mon Sep 17 00:00:00 2001 From: David F Date: Thu, 14 May 2026 00:38:50 +0200 Subject: [PATCH 01/16] Add custom image/GIF support to icon state customizations --- actions/show_icon/icon_const.py | 4 ++ actions/show_icon/icon_customization.py | 16 +++++- actions/show_icon/icon_helper.py | 27 ++++++--- actions/show_icon/icon_window.py | 73 ++++++++++++++++++++++++- locales/de_DE.json | 2 + locales/en_US.json | 2 + 6 files changed, 110 insertions(+), 14 deletions(-) diff --git a/actions/show_icon/icon_const.py b/actions/show_icon/icon_const.py index fef9395..f7d422e 100644 --- a/actions/show_icon/icon_const.py +++ b/actions/show_icon/icon_const.py @@ -31,6 +31,10 @@ CUSTOM_COLOR = "color" CUSTOM_SCALE = "scale" CUSTOM_OPACITY = "opacity" +CUSTOM_IMAGE = "custom_image" + +LABEL_ICON_IMAGE = "actions.home_assistant.icon.image.label" +LABEL_ICON_BROWSE = "actions.home_assistant.icon.browse.label" MDI_SVG_JSON = "assets/mdi-svg.json" ICON_COLOR_RED = "#ff0000" diff --git a/actions/show_icon/icon_customization.py b/actions/show_icon/icon_customization.py index 93ec74f..f5cf4bf 100644 --- a/actions/show_icon/icon_customization.py +++ b/actions/show_icon/icon_customization.py @@ -15,12 +15,13 @@ class IconCustomization(Customization): """ def __init__(self, attribute: str, operator: str, value: str, icon: str, color: tuple[int, int, int, int], - scale: int, opacity: int): + scale: int, opacity: int, custom_image: str = None): super().__init__(attribute, operator, value) self.icon: str = icon self.color: tuple[int, int, int, int] = color self.scale: int = scale self.opacity: int = opacity + self.custom_image: str = custom_image @classmethod def from_dict(cls, customization: dict): @@ -28,7 +29,8 @@ def from_dict(cls, customization: dict): customization[customization_const.CONDITION][customization_const.OPERATOR], customization[customization_const.CONDITION][customization_const.VALUE], customization[icon_const.CUSTOM_ICON], customization[icon_const.CUSTOM_COLOR], - customization[icon_const.CUSTOM_SCALE], customization[icon_const.CUSTOM_OPACITY]) + customization[icon_const.CUSTOM_SCALE], customization[icon_const.CUSTOM_OPACITY], + customization.get(icon_const.CUSTOM_IMAGE)) def get_icon(self) -> str: """ @@ -58,6 +60,13 @@ def get_opacity(self) -> int: """ return self.opacity + def get_custom_image(self) -> str: + """ + Get the custom image path. + :return: path to a custom image/GIF file, or None + """ + return self.custom_image + def export(self) -> dict[str, Any]: """ Get this customization as a dict. @@ -72,5 +81,6 @@ def export(self) -> dict[str, Any]: icon_const.CUSTOM_ICON: self.icon, icon_const.CUSTOM_COLOR: self.color, icon_const.CUSTOM_SCALE: self.scale, - icon_const.CUSTOM_OPACITY: self.opacity + icon_const.CUSTOM_OPACITY: self.opacity, + icon_const.CUSTOM_IMAGE: self.custom_image } diff --git a/actions/show_icon/icon_helper.py b/actions/show_icon/icon_helper.py index d052add..7476526 100644 --- a/actions/show_icon/icon_helper.py +++ b/actions/show_icon/icon_helper.py @@ -27,7 +27,10 @@ def get_icon(state: dict, settings: ShowIconSettings, is_connected: bool) -> tup icon_const.ICON_COLOR_RED).replace( "", "1.0")), round(icon_const.DEFAULT_ICON_SCALE / 100, 2) - name, color, scale, opacity = _get_icon_settings(state, settings) + name, color, scale, opacity, custom_image = _get_icon_settings(state, settings) + + if custom_image: + return custom_image, scale # convert RGB color to hex color = customization_helper.convert_color_list_to_hex(color) @@ -37,12 +40,13 @@ def get_icon(state: dict, settings: ShowIconSettings, is_connected: bool) -> tup return (icon.replace("", color).replace("", str(opacity))), scale -def _get_icon_settings(state: dict, settings: ShowIconSettings) -> tuple[str, str, float, float]: +def _get_icon_settings(state: dict, settings: ShowIconSettings) -> tuple[str, str, float, float, str]: # default value for the icon is the icon set in HA name = state.get(icon_const.ATTRIBUTES, {}).get(icon_const.ATTRIBUTE_ICON, icon_const.EMPTY_STRING) color = settings.get_color() scale = settings.get_scale() opacity = settings.get_opacity() + custom_image = None if settings.get_icon() in MDI_ICONS.keys(): name = settings.get_icon() @@ -70,8 +74,8 @@ def _get_icon_settings(state: dict, settings: ShowIconSettings) -> tuple[str, st if ((operator == "==" and str(value) == str(custom_icon_value)) or (operator == "!=" and str(value) != str(custom_icon_value))): - name, color, scale, opacity = _replace_values(name, color, scale, opacity, - customization) + name, color, scale, opacity, custom_image = _replace_values(name, color, scale, opacity, + custom_image, customization) if not isinstance(value, float): # other operators are only applicable to numbers @@ -88,8 +92,8 @@ def _get_icon_settings(state: dict, settings: ShowIconSettings) -> tuple[str, st or (operator == "<=" and value <= custom_icon_value) or (operator == ">" and value > custom_icon_value) or (operator == ">=" and value >= custom_icon_value)): - name, color, scale, opacity = _replace_values(name, color, scale, opacity, - customization) + name, color, scale, opacity, custom_image = _replace_values(name, color, scale, opacity, + custom_image, customization) # # End custom icon @@ -97,7 +101,7 @@ def _get_icon_settings(state: dict, settings: ShowIconSettings) -> tuple[str, st scale = round(scale / 100, 2) opacity = round(opacity / 100, 2) - return name, color, scale, opacity + return name, color, scale, opacity, custom_image def get_value(state: dict, customization: IconCustomization): @@ -112,11 +116,13 @@ def get_value(state: dict, customization: IconCustomization): return value -def _replace_values(name: str, color: str, scale: float, opacity: str, customization: IconCustomization): +def _replace_values(name: str, color: str, scale: float, opacity: str, custom_image: str, + customization: IconCustomization): ret_name = name ret_color = color ret_scale = scale ret_opacity = opacity + ret_custom_image = custom_image if customization.get_icon() is not None: ret_name = customization.get_icon() @@ -130,7 +136,10 @@ def _replace_values(name: str, color: str, scale: float, opacity: str, customiza if customization.get_opacity() is not None: ret_opacity = customization.get_opacity() - return ret_name, ret_color, ret_scale, ret_opacity + if customization.get_custom_image() is not None: + ret_custom_image = customization.get_custom_image() + + return ret_name, ret_color, ret_scale, ret_opacity, ret_custom_image def _get_icon_path(name: str) -> str: diff --git a/actions/show_icon/icon_window.py b/actions/show_icon/icon_window.py index c4b8215..48d5e0c 100644 --- a/actions/show_icon/icon_window.py +++ b/actions/show_icon/icon_window.py @@ -2,9 +2,15 @@ The module for the Home Assistant customization icon window. """ +import os from functools import partial from typing import Callable +import gi +gi.require_version("Gtk", "4.0") +from gi.repository import Gio +from gi.repository.Gtk import Box, Button, FileDialog, FileFilter + from HomeAssistantPlugin.actions import const as base_const from HomeAssistantPlugin.actions.cores.customization_core import customization_helper from HomeAssistantPlugin.actions.cores.customization_core.customization_window import CustomizationWindow @@ -29,17 +35,34 @@ def __init__(self, lm, attributes: list, callback: Callable, self.check_color = self._create_check_button() self.check_scale = self._create_check_button() self.check_opacity = self._create_check_button() + self.check_image = self._create_check_button() label_icon = self._create_label(self.lm.get(icon_const.LABEL_ICON_ICON)) label_color = self._create_label(self.lm.get(icon_const.LABEL_ICON_COLOR)) label_scale = self._create_label(self.lm.get(icon_const.LABEL_ICON_SCALE)) label_opacity = self._create_label(self.lm.get(icon_const.LABEL_ICON_OPACITY)) + label_image = self._create_label(self.lm.get(icon_const.LABEL_ICON_IMAGE)) self.icon = self._create_entry(self.check_icon) self.icon.set_margin_end(self.default_margin) self.connect_rows.append( partial(self.icon.connect, base_const.CONNECT_ACTIVATE, self.on_widget_changed)) + self.image_entry = self._create_entry(self.check_image) + self.image_entry.set_hexpand(True) + self.image_entry.set_margin_end(self.default_margin) + + browse_button = Button(label=self.lm.get(icon_const.LABEL_ICON_BROWSE)) + browse_button.set_margin_top(self.default_margin) + browse_button.set_margin_bottom(self.default_margin) + browse_button.set_margin_start(self.default_margin) + browse_button.set_margin_end(self.default_margin) + self.connect_rows.append(partial(browse_button.connect, base_const.CONNECT_CLICKED, self._on_browse_clicked)) + + image_box = Box() + image_box.append(self.image_entry) + image_box.append(browse_button) + self.color = self._create_color_button(self.check_color) self.scale = self._create_scale(icon_const.ICON_MIN_SCALE, icon_const.ICON_MAX_SCALE, 1, @@ -90,6 +113,10 @@ def __init__(self, lm, attributes: list, callback: Callable, self.grid_fields.attach(self.opacity, 4, 5, 1, 1) self.grid_fields.attach(self.opacity_entry, 5, 5, 1, 1) + self.grid_fields.attach(self.check_image, 2, 6, 1, 1) + self.grid_fields.attach(label_image, 3, 6, 1, 1) + self.grid_fields.attach(image_box, 4, 6, 2, 1) + self._after_init() def set_default_values(self) -> None: @@ -130,6 +157,9 @@ def set_current_values(self) -> None: self.opacity_entry.set_text( str(int(self.current.get_opacity() or icon_const.DEFAULT_ICON_OPACITY))) + self.image_entry.set_text(self.current.get_custom_image() or icon_const.EMPTY_STRING) + self.check_image.set_active(self.current.get_custom_image() is not None) + def on_add_button(self, *_, **__) -> None: if not super().on_add_button(): return @@ -144,12 +174,18 @@ def on_add_button(self, *_, **__) -> None: self.icon.add_css_class(icon_const.ERROR) return + if self.check_image.get_active() and not self.image_entry.get_text(): + self.image_entry.add_css_class(icon_const.ERROR) + return + if (not self.check_icon.get_active() and not self.check_color.get_active() and not - self.check_scale.get_active() and not self.check_opacity.get_active()): + self.check_scale.get_active() and not self.check_opacity.get_active() and not + self.check_image.get_active()): self.check_icon.add_css_class(icon_const.ERROR) self.check_color.add_css_class(icon_const.ERROR) self.check_scale.add_css_class(icon_const.ERROR) self.check_opacity.add_css_class(icon_const.ERROR) + self.check_image.add_css_class(icon_const.ERROR) return icon = self.icon.get_text() if self.check_icon.get_active() else None @@ -158,15 +194,46 @@ def on_add_button(self, *_, **__) -> None: scale = int(self.scale.get_value()) if self.check_scale.get_active() else None opacity = int( self.opacity.get_value()) if self.check_opacity.get_active() else None + custom_image = self.image_entry.get_text() if self.check_image.get_active() else None self.callback(customization=IconCustomization( attribute=self.condition_attribute.get_selected_item().get_string(), operator=self.operator.get_selected_item().value, value=self.entry_value.get_text(), icon=icon, color=color_list, scale=scale, - opacity=opacity), index=self.index) + opacity=opacity, custom_image=custom_image), index=self.index) self.destroy() + def _on_browse_clicked(self, *_) -> None: + dialog = FileDialog() + dialog.set_title(self.lm.get(icon_const.LABEL_ICON_IMAGE)) + + filter_images = FileFilter() + filter_images.set_name("Images") + for mime in ("image/png", "image/jpeg", "image/gif", "image/webp", "image/svg+xml"): + filter_images.add_mime_type(mime) + + filters = Gio.ListStore.new(FileFilter) + filters.append(filter_images) + dialog.set_filters(filters) + dialog.set_default_filter(filter_images) + + if self.image_entry.get_text(): + current_dir = os.path.dirname(self.image_entry.get_text()) + if os.path.isdir(current_dir): + dialog.set_initial_folder(Gio.File.new_for_path(current_dir)) + + dialog.open(self, None, self._on_file_chosen) + + def _on_file_chosen(self, dialog, result) -> None: + try: + file = dialog.open_finish(result) + if file: + self.image_entry.set_text(file.get_path()) + self.check_image.set_active(True) + except Exception: + pass + def on_widget_changed(self, *_, **__) -> None: super().on_widget_changed() @@ -175,3 +242,5 @@ def on_widget_changed(self, *_, **__) -> None: self.check_color.remove_css_class(icon_const.ERROR) self.check_scale.remove_css_class(icon_const.ERROR) self.check_opacity.remove_css_class(icon_const.ERROR) + self.image_entry.remove_css_class(icon_const.ERROR) + self.check_image.remove_css_class(icon_const.ERROR) diff --git a/locales/de_DE.json b/locales/de_DE.json index b889e60..f522679 100644 --- a/locales/de_DE.json +++ b/locales/de_DE.json @@ -28,6 +28,8 @@ "actions.home_assistant.icon.opacity.label": "Deckkraft:", "actions.home_assistant.icon.scale.label": "Größe:", "actions.home_assistant.icon.custom_icon.label": "Anpassen:", + "actions.home_assistant.icon.image.label": "Bild:", + "actions.home_assistant.icon.browse.label": "Durchsuchen...", "actions.home_assistant.icon.no_entity.label": "Keine Entität ausgewählt", "actions.home_assistant.text.show_text.label": "Text anzeigen:", diff --git a/locales/en_US.json b/locales/en_US.json index d921814..07e73d0 100644 --- a/locales/en_US.json +++ b/locales/en_US.json @@ -28,6 +28,8 @@ "actions.home_assistant.icon.opacity.label": "Opacity:", "actions.home_assistant.icon.scale.label": "Scale:", "actions.home_assistant.icon.custom_icon.label": "Customize:", + "actions.home_assistant.icon.image.label": "Image:", + "actions.home_assistant.icon.browse.label": "Browse...", "actions.home_assistant.icon.no_entity.label": "No entity selected", "actions.home_assistant.text.show_text.label": "Show text:", From 61479dfda66b4b4ac3aa2a71bcc81064e01f28ca Mon Sep 17 00:00:00 2001 From: David F Date: Thu, 14 May 2026 14:18:16 +0200 Subject: [PATCH 02/16] Add custom image/GIF support to main action settings Extends the existing image customization (previously only available in state filters) to the main Show Icon action settings. A custom image overrides the icon, color, scale and opacity when set. The other fields are disabled when an image is active to make the relationship clear. Also hardens settings initialization to fill in missing keys from defaults, preventing KeyErrors on actions with incomplete saved state. --- actions/show_icon/icon_action.py | 68 +++++++++++++++++++++++++++--- actions/show_icon/icon_const.py | 3 ++ actions/show_icon/icon_helper.py | 2 +- actions/show_icon/icon_settings.py | 22 +++++++++- actions/show_icon/icon_window.py | 22 +++++++++- locales/de_DE.json | 1 + locales/en_US.json | 1 + 7 files changed, 109 insertions(+), 10 deletions(-) diff --git a/actions/show_icon/icon_action.py b/actions/show_icon/icon_action.py index 3a2fdb6..9c6b225 100644 --- a/actions/show_icon/icon_action.py +++ b/actions/show_icon/icon_action.py @@ -2,6 +2,13 @@ The module for the Home Assistant action that is loaded in StreamController. """ +import os + +import gi +gi.require_version("Gtk", "4.0") +from gi.repository import Gio +from gi.repository.Gtk import Align, Button, FileDialog, FileFilter + from HomeAssistantPlugin.actions.cores.base_core.base_core import requires_initialization from HomeAssistantPlugin.actions.cores.customization_core.customization_core import CustomizationCore from HomeAssistantPlugin.actions.show_icon import icon_const, icon_helper @@ -24,6 +31,7 @@ def __init__(self, *args, **kwargs): self.color = None self.scale = None self.opacity = None + self.custom_image = None super().__init__(window_implementation=IconWindow, customization_implementation=IconCustomization, row_implementation=IconRow, settings_implementation=ShowIconSettings, track_entity=True, *args, **kwargs) @@ -31,7 +39,7 @@ def __init__(self, *args, **kwargs): def get_config_rows(self) -> list: """Get the rows to be displayed in the UI.""" return [self.domain_combo.widget, self.entity_combo.widget, self.icon.widget, self.color.widget, - self.scale.widget, self.opacity.widget, self.customization_expander.widget] + self.scale.widget, self.opacity.widget, self.custom_image.widget, self.customization_expander.widget] def create_ui_elements(self) -> None: """Get all action rows.""" @@ -63,6 +71,19 @@ def create_ui_elements(self) -> None: can_reset=False, complex_var_name=True ) + image_label = self.lm.get(icon_const.LABEL_ICON_IMAGE).rstrip(":") + image_overrides = self.lm.get(icon_const.LABEL_ICON_IMAGE_OVERRIDES) + self.custom_image: EntryRow = EntryRow( + self, icon_const.SETTING_ICON_CUSTOM_IMAGE, icon_const.EMPTY_STRING, + title=f"{image_label} ({image_overrides}):", on_change=self._reload, + can_reset=True, complex_var_name=True + ) + + browse_button = Button(label=self.lm.get(icon_const.LABEL_ICON_BROWSE)) + browse_button.set_valign(Align.CENTER) + browse_button.connect("clicked", self._on_browse_clicked) + self.custom_image.widget.add_suffix(browse_button) + @requires_initialization def set_enabled_disabled(self) -> None: """ @@ -87,16 +108,22 @@ def set_enabled_disabled(self) -> None: self.opacity.widget.set_sensitive(False) self.opacity.widget.set_subtitle(self.lm.get(icon_const.LABEL_ICON_NO_ENTITY)) + + self.custom_image.widget.set_sensitive(False) else: - self.icon.widget.set_sensitive(True) + self.custom_image.widget.set_sensitive(True) + + has_image = bool(self.settings.get_custom_image()) - self.color.widget.set_sensitive(True) + self.icon.widget.set_sensitive(not has_image) + + self.color.widget.set_sensitive(not has_image) self.color.widget.set_subtitle(icon_const.EMPTY_STRING) - self.scale.widget.set_sensitive(True) + self.scale.widget.set_sensitive(not has_image) self.scale.widget.set_subtitle(icon_const.EMPTY_STRING) - self.opacity.widget.set_sensitive(True) + self.opacity.widget.set_sensitive(not has_image) self.opacity.widget.set_subtitle(icon_const.EMPTY_STRING) def refresh(self, state: dict = None) -> None: @@ -123,6 +150,37 @@ def refresh(self, state: dict = None) -> None: self._load_customizations() self.set_enabled_disabled() + def _on_browse_clicked(self, *_) -> None: + dialog = FileDialog() + dialog.set_title(self.lm.get(icon_const.LABEL_ICON_IMAGE)) + + filter_images = FileFilter() + filter_images.set_name("Images") + for mime in ("image/png", "image/jpeg", "image/gif", "image/webp", "image/svg+xml"): + filter_images.add_mime_type(mime) + + filters = Gio.ListStore.new(FileFilter) + filters.append(filter_images) + dialog.set_filters(filters) + dialog.set_default_filter(filter_images) + + current_path = self.custom_image.widget.get_text() + if current_path: + current_dir = os.path.dirname(current_path) + if os.path.isdir(current_dir): + dialog.set_initial_folder(Gio.File.new_for_path(current_dir)) + + dialog.open(None, None, self._on_file_chosen) + + def _on_file_chosen(self, dialog, result) -> None: + try: + file = dialog.open_finish(result) + if file: + self.custom_image.widget.set_text(file.get_path()) + self._reload() + except Exception: + pass + def _get_domains(self) -> list[str]: """This class needs all domains that provide actions in Home Assistant.""" return self.plugin_base.backend.get_domains_for_entities() diff --git a/actions/show_icon/icon_const.py b/actions/show_icon/icon_const.py index f7d422e..c81e1d2 100644 --- a/actions/show_icon/icon_const.py +++ b/actions/show_icon/icon_const.py @@ -33,7 +33,10 @@ CUSTOM_OPACITY = "opacity" CUSTOM_IMAGE = "custom_image" +SETTING_ICON_CUSTOM_IMAGE = f"{SETTING_ICON}.{CUSTOM_IMAGE}" + LABEL_ICON_IMAGE = "actions.home_assistant.icon.image.label" +LABEL_ICON_IMAGE_OVERRIDES = "actions.home_assistant.icon.image_overrides.label" LABEL_ICON_BROWSE = "actions.home_assistant.icon.browse.label" MDI_SVG_JSON = "assets/mdi-svg.json" diff --git a/actions/show_icon/icon_helper.py b/actions/show_icon/icon_helper.py index 7476526..0244d9e 100644 --- a/actions/show_icon/icon_helper.py +++ b/actions/show_icon/icon_helper.py @@ -46,7 +46,7 @@ def _get_icon_settings(state: dict, settings: ShowIconSettings) -> tuple[str, st color = settings.get_color() scale = settings.get_scale() opacity = settings.get_opacity() - custom_image = None + custom_image = settings.get_custom_image() or None if settings.get_icon() in MDI_ICONS.keys(): name = settings.get_icon() diff --git a/actions/show_icon/icon_settings.py b/actions/show_icon/icon_settings.py index 68304d1..2faa4b9 100644 --- a/actions/show_icon/icon_settings.py +++ b/actions/show_icon/icon_settings.py @@ -10,6 +10,7 @@ icon_const.SETTING_COLOR: icon_const.DEFAULT_ICON_COLOR, icon_const.SETTING_SCALE: icon_const.DEFAULT_ICON_SCALE, icon_const.SETTING_OPACITY: icon_const.DEFAULT_ICON_OPACITY, + icon_const.CUSTOM_IMAGE: icon_const.EMPTY_STRING, customization_const.SETTING_CUSTOMIZATIONS: [] } @@ -23,10 +24,20 @@ class ShowIconSettings(CustomizationSettings): def __init__(self, action): super().__init__(action, icon_const.SETTING_ICON, IconCustomization) - if not self._action.get_settings().get(icon_const.SETTING_ICON): - settings = self._action.get_settings() + settings = self._action.get_settings() + icon_settings = settings.get(icon_const.SETTING_ICON) + if not icon_settings: settings[icon_const.SETTING_ICON] = DEFAULT_SETTINGS.copy() self._action.set_settings(settings) + else: + # Fill in any missing keys from DEFAULT_SETTINGS + changed = False + for key, value in DEFAULT_SETTINGS.items(): + if key not in icon_settings: + icon_settings[key] = value + changed = True + if changed: + self._action.set_settings(settings) def get_icon(self) -> str: """ @@ -55,3 +66,10 @@ def get_opacity(self) -> int: :return: the opacity """ return int(self._action.get_settings()[icon_const.SETTING_ICON][icon_const.SETTING_OPACITY]) + + def get_custom_image(self) -> str: + """ + Get the custom image path. + :return: the custom image path + """ + return self._action.get_settings()[icon_const.SETTING_ICON].get(icon_const.CUSTOM_IMAGE, icon_const.EMPTY_STRING) diff --git a/actions/show_icon/icon_window.py b/actions/show_icon/icon_window.py index 48d5e0c..8539783 100644 --- a/actions/show_icon/icon_window.py +++ b/actions/show_icon/icon_window.py @@ -9,7 +9,7 @@ import gi gi.require_version("Gtk", "4.0") from gi.repository import Gio -from gi.repository.Gtk import Box, Button, FileDialog, FileFilter +from gi.repository.Gtk import Box, Button, FileDialog, FileFilter, Label, Orientation from HomeAssistantPlugin.actions import const as base_const from HomeAssistantPlugin.actions.cores.customization_core import customization_helper @@ -58,11 +58,21 @@ def __init__(self, lm, attributes: list, callback: Callable, browse_button.set_margin_start(self.default_margin) browse_button.set_margin_end(self.default_margin) self.connect_rows.append(partial(browse_button.connect, base_const.CONNECT_CLICKED, self._on_browse_clicked)) + self.connect_rows.append(partial(self.check_image.connect, "toggled", self._on_check_image_toggled)) image_box = Box() image_box.append(self.image_entry) image_box.append(browse_button) + image_hint_label = Label(label=self.lm.get(icon_const.LABEL_ICON_IMAGE_OVERRIDES)) + image_hint_label.add_css_class("caption") + image_hint_label.add_css_class("dim-label") + image_hint_label.set_xalign(0) + + image_col_box = Box(orientation=Orientation.VERTICAL) + image_col_box.append(image_box) + image_col_box.append(image_hint_label) + self.color = self._create_color_button(self.check_color) self.scale = self._create_scale(icon_const.ICON_MIN_SCALE, icon_const.ICON_MAX_SCALE, 1, @@ -115,7 +125,7 @@ def __init__(self, lm, attributes: list, callback: Callable, self.grid_fields.attach(self.check_image, 2, 6, 1, 1) self.grid_fields.attach(label_image, 3, 6, 1, 1) - self.grid_fields.attach(image_box, 4, 6, 2, 1) + self.grid_fields.attach(image_col_box, 4, 6, 2, 1) self._after_init() @@ -159,6 +169,7 @@ def set_current_values(self) -> None: self.image_entry.set_text(self.current.get_custom_image() or icon_const.EMPTY_STRING) self.check_image.set_active(self.current.get_custom_image() is not None) + self._on_check_image_toggled() def on_add_button(self, *_, **__) -> None: if not super().on_add_button(): @@ -204,6 +215,13 @@ def on_add_button(self, *_, **__) -> None: self.destroy() + def _on_check_image_toggled(self, *_) -> None: + has_image = self.check_image.get_active() + for widget in [self.check_icon, self.icon, self.check_color, self.color, + self.check_scale, self.scale, self.scale_entry, + self.check_opacity, self.opacity, self.opacity_entry]: + widget.set_sensitive(not has_image) + def _on_browse_clicked(self, *_) -> None: dialog = FileDialog() dialog.set_title(self.lm.get(icon_const.LABEL_ICON_IMAGE)) diff --git a/locales/de_DE.json b/locales/de_DE.json index f522679..a7b283e 100644 --- a/locales/de_DE.json +++ b/locales/de_DE.json @@ -29,6 +29,7 @@ "actions.home_assistant.icon.scale.label": "Größe:", "actions.home_assistant.icon.custom_icon.label": "Anpassen:", "actions.home_assistant.icon.image.label": "Bild:", + "actions.home_assistant.icon.image_overrides.label": "Überschreibt Icon, Farbe, Größe und Deckkraft", "actions.home_assistant.icon.browse.label": "Durchsuchen...", "actions.home_assistant.icon.no_entity.label": "Keine Entität ausgewählt", diff --git a/locales/en_US.json b/locales/en_US.json index 07e73d0..8fcddb2 100644 --- a/locales/en_US.json +++ b/locales/en_US.json @@ -29,6 +29,7 @@ "actions.home_assistant.icon.scale.label": "Scale:", "actions.home_assistant.icon.custom_icon.label": "Customize:", "actions.home_assistant.icon.image.label": "Image:", + "actions.home_assistant.icon.image_overrides.label": "Overrides icon, color, scale and opacity", "actions.home_assistant.icon.browse.label": "Browse...", "actions.home_assistant.icon.no_entity.label": "No entity selected", From 1cb71489a6b1a61df321a07ff2622f692e88c288 Mon Sep 17 00:00:00 2001 From: gensyn Date: Thu, 14 May 2026 21:56:08 +0200 Subject: [PATCH 03/16] Changed "custom_image" to "image", cleaned up code, fixed customization row --- actions/cores/base_core/base_core.py | 5 ++++- actions/cores/base_core/migrate.py | 28 +++++++++++++++++++++++++ actions/show_icon/icon_action.py | 27 ++++++++++++------------ actions/show_icon/icon_const.py | 8 +++---- actions/show_icon/icon_customization.py | 18 ++++++++-------- actions/show_icon/icon_helper.py | 28 ++++++++++++------------- actions/show_icon/icon_row.py | 4 ++++ actions/show_icon/icon_settings.py | 10 ++++----- actions/show_icon/icon_window.py | 9 ++++---- 9 files changed, 86 insertions(+), 51 deletions(-) create mode 100644 actions/cores/base_core/migrate.py diff --git a/actions/cores/base_core/base_core.py b/actions/cores/base_core/base_core.py index 2f6c41e..d0b54e5 100644 --- a/actions/cores/base_core/base_core.py +++ b/actions/cores/base_core/base_core.py @@ -1,8 +1,10 @@ """The module for the Home Assistant action that is loaded in StreamController.""" import gi -from GtkHelper.GenerativeUI.ComboRow import ComboRow from HomeAssistantPlugin.actions import const +from HomeAssistantPlugin.actions.cores.base_core.migrate import migrate + +from GtkHelper.GenerativeUI.ComboRow import ComboRow from src.backend.PluginManager.ActionCore import ActionCore gi.require_version('Gtk', '4.0') @@ -48,6 +50,7 @@ def __init__(self, settings_implementation, track_entity: bool, *args, **kwargs) def on_ready(self) -> None: """Set up action when StreamController has finished loading.""" + migrate(self) self.settings = self.settings_implementation(self) self.initialized = True diff --git a/actions/cores/base_core/migrate.py b/actions/cores/base_core/migrate.py new file mode 100644 index 0000000..ddf5b25 --- /dev/null +++ b/actions/cores/base_core/migrate.py @@ -0,0 +1,28 @@ +from HomeAssistantPlugin.actions import const +from HomeAssistantPlugin.actions.cores.customization_core import customization_const +from HomeAssistantPlugin.actions.show_icon import icon_const +from HomeAssistantPlugin.actions.show_icon.icon_settings import ShowIconSettings + + +def migrate(action) -> None: + settings = action.get_settings() + + if settings.get(const.SETTING_VERSION, 0) > 1: + return + + migrate_v1_v2(action) + +def migrate_v1_v2(action) -> None: + settings = action.get_settings() + + settings[const.SETTING_VERSION] = 2 + action.set_settings(settings) + + if action.__class__.__name__ != "ShowIcon": + return + + settings[icon_const.SETTING_ICON][icon_const.SETTING_IMAGE] = icon_const.EMPTY_STRING + for customization in settings[icon_const.SETTING_ICON][customization_const.SETTING_CUSTOMIZATIONS]: + customization[icon_const.CUSTOM_IMAGE] = icon_const.EMPTY_STRING + + action.set_settings(settings) \ No newline at end of file diff --git a/actions/show_icon/icon_action.py b/actions/show_icon/icon_action.py index 9c6b225..3307479 100644 --- a/actions/show_icon/icon_action.py +++ b/actions/show_icon/icon_action.py @@ -5,6 +5,7 @@ import os import gi + gi.require_version("Gtk", "4.0") from gi.repository import Gio from gi.repository.Gtk import Align, Button, FileDialog, FileFilter @@ -31,7 +32,7 @@ def __init__(self, *args, **kwargs): self.color = None self.scale = None self.opacity = None - self.custom_image = None + self.image = None super().__init__(window_implementation=IconWindow, customization_implementation=IconCustomization, row_implementation=IconRow, settings_implementation=ShowIconSettings, track_entity=True, *args, **kwargs) @@ -39,7 +40,7 @@ def __init__(self, *args, **kwargs): def get_config_rows(self) -> list: """Get the rows to be displayed in the UI.""" return [self.domain_combo.widget, self.entity_combo.widget, self.icon.widget, self.color.widget, - self.scale.widget, self.opacity.widget, self.custom_image.widget, self.customization_expander.widget] + self.scale.widget, self.opacity.widget, self.image.widget, self.customization_expander.widget] def create_ui_elements(self) -> None: """Get all action rows.""" @@ -71,18 +72,16 @@ def create_ui_elements(self) -> None: can_reset=False, complex_var_name=True ) - image_label = self.lm.get(icon_const.LABEL_ICON_IMAGE).rstrip(":") - image_overrides = self.lm.get(icon_const.LABEL_ICON_IMAGE_OVERRIDES) - self.custom_image: EntryRow = EntryRow( - self, icon_const.SETTING_ICON_CUSTOM_IMAGE, icon_const.EMPTY_STRING, - title=f"{image_label} ({image_overrides}):", on_change=self._reload, - can_reset=True, complex_var_name=True + self.image: EntryRow = EntryRow( + self, icon_const.SETTING_ICON_IMAGE, icon_const.EMPTY_STRING, + title=icon_const.LABEL_ICON_IMAGE, on_change=self._reload, + can_reset=False, complex_var_name=True ) browse_button = Button(label=self.lm.get(icon_const.LABEL_ICON_BROWSE)) browse_button.set_valign(Align.CENTER) browse_button.connect("clicked", self._on_browse_clicked) - self.custom_image.widget.add_suffix(browse_button) + self.image.widget.add_suffix(browse_button) @requires_initialization def set_enabled_disabled(self) -> None: @@ -109,11 +108,11 @@ def set_enabled_disabled(self) -> None: self.opacity.widget.set_sensitive(False) self.opacity.widget.set_subtitle(self.lm.get(icon_const.LABEL_ICON_NO_ENTITY)) - self.custom_image.widget.set_sensitive(False) + self.image.widget.set_sensitive(False) else: - self.custom_image.widget.set_sensitive(True) + self.image.widget.set_sensitive(True) - has_image = bool(self.settings.get_custom_image()) + has_image = bool(self.settings.get_image()) self.icon.widget.set_sensitive(not has_image) @@ -164,7 +163,7 @@ def _on_browse_clicked(self, *_) -> None: dialog.set_filters(filters) dialog.set_default_filter(filter_images) - current_path = self.custom_image.widget.get_text() + current_path = self.image.widget.get_text() if current_path: current_dir = os.path.dirname(current_path) if os.path.isdir(current_dir): @@ -176,7 +175,7 @@ def _on_file_chosen(self, dialog, result) -> None: try: file = dialog.open_finish(result) if file: - self.custom_image.widget.set_text(file.get_path()) + self.image.widget.set_text(file.get_path()) self._reload() except Exception: pass diff --git a/actions/show_icon/icon_const.py b/actions/show_icon/icon_const.py index c81e1d2..2f9f70d 100644 --- a/actions/show_icon/icon_const.py +++ b/actions/show_icon/icon_const.py @@ -13,10 +13,12 @@ SETTING_COLOR = "color" SETTING_SCALE = "scale" SETTING_OPACITY = "opacity" +SETTING_IMAGE = "image" SETTING_ICON_ICON = f"{SETTING_ICON}.{SETTING_ICON}" SETTING_ICON_COLOR = f"{SETTING_ICON}.{SETTING_COLOR}" SETTING_ICON_SCALE = f"{SETTING_ICON}.{SETTING_SCALE}" SETTING_ICON_OPACITY = f"{SETTING_ICON}.{SETTING_OPACITY}" +SETTING_ICON_IMAGE = f"{SETTING_ICON}.{SETTING_IMAGE}" DEFAULT_ICON_COLOR = [237, 255, 26, 255] DEFAULT_ICON_SCALE = 80 @@ -31,9 +33,7 @@ CUSTOM_COLOR = "color" CUSTOM_SCALE = "scale" CUSTOM_OPACITY = "opacity" -CUSTOM_IMAGE = "custom_image" - -SETTING_ICON_CUSTOM_IMAGE = f"{SETTING_ICON}.{CUSTOM_IMAGE}" +CUSTOM_IMAGE = "image" LABEL_ICON_IMAGE = "actions.home_assistant.icon.image.label" LABEL_ICON_IMAGE_OVERRIDES = "actions.home_assistant.icon.image_overrides.label" @@ -46,4 +46,4 @@ ATTRIBUTES = "attributes" ATTRIBUTE_ICON = "icon" STATE = "state" -ERROR = "error" \ No newline at end of file +ERROR = "error" diff --git a/actions/show_icon/icon_customization.py b/actions/show_icon/icon_customization.py index f5cf4bf..d4d0938 100644 --- a/actions/show_icon/icon_customization.py +++ b/actions/show_icon/icon_customization.py @@ -1,5 +1,5 @@ """ -Modul to manage icon customizations. +Module to manage icon customizations. """ from typing import Any @@ -15,13 +15,13 @@ class IconCustomization(Customization): """ def __init__(self, attribute: str, operator: str, value: str, icon: str, color: tuple[int, int, int, int], - scale: int, opacity: int, custom_image: str = None): + scale: int, opacity: int, image: str): super().__init__(attribute, operator, value) self.icon: str = icon self.color: tuple[int, int, int, int] = color self.scale: int = scale self.opacity: int = opacity - self.custom_image: str = custom_image + self.image: str = image @classmethod def from_dict(cls, customization: dict): @@ -30,7 +30,7 @@ def from_dict(cls, customization: dict): customization[customization_const.CONDITION][customization_const.VALUE], customization[icon_const.CUSTOM_ICON], customization[icon_const.CUSTOM_COLOR], customization[icon_const.CUSTOM_SCALE], customization[icon_const.CUSTOM_OPACITY], - customization.get(icon_const.CUSTOM_IMAGE)) + customization[icon_const.CUSTOM_IMAGE]) def get_icon(self) -> str: """ @@ -60,12 +60,12 @@ def get_opacity(self) -> int: """ return self.opacity - def get_custom_image(self) -> str: + def get_image(self) -> str: """ - Get the custom image path. - :return: path to a custom image/GIF file, or None + Get the image path. + :return: path to an image/GIF file, or None """ - return self.custom_image + return self.image def export(self) -> dict[str, Any]: """ @@ -82,5 +82,5 @@ def export(self) -> dict[str, Any]: icon_const.CUSTOM_COLOR: self.color, icon_const.CUSTOM_SCALE: self.scale, icon_const.CUSTOM_OPACITY: self.opacity, - icon_const.CUSTOM_IMAGE: self.custom_image + icon_const.CUSTOM_IMAGE: self.image } diff --git a/actions/show_icon/icon_helper.py b/actions/show_icon/icon_helper.py index 0244d9e..7fae403 100644 --- a/actions/show_icon/icon_helper.py +++ b/actions/show_icon/icon_helper.py @@ -27,10 +27,10 @@ def get_icon(state: dict, settings: ShowIconSettings, is_connected: bool) -> tup icon_const.ICON_COLOR_RED).replace( "", "1.0")), round(icon_const.DEFAULT_ICON_SCALE / 100, 2) - name, color, scale, opacity, custom_image = _get_icon_settings(state, settings) + name, color, scale, opacity, image = _get_icon_settings(state, settings) - if custom_image: - return custom_image, scale + if image: + return image, scale # convert RGB color to hex color = customization_helper.convert_color_list_to_hex(color) @@ -46,7 +46,7 @@ def _get_icon_settings(state: dict, settings: ShowIconSettings) -> tuple[str, st color = settings.get_color() scale = settings.get_scale() opacity = settings.get_opacity() - custom_image = settings.get_custom_image() or None + image = settings.get_image() or None if settings.get_icon() in MDI_ICONS.keys(): name = settings.get_icon() @@ -74,8 +74,8 @@ def _get_icon_settings(state: dict, settings: ShowIconSettings) -> tuple[str, st if ((operator == "==" and str(value) == str(custom_icon_value)) or (operator == "!=" and str(value) != str(custom_icon_value))): - name, color, scale, opacity, custom_image = _replace_values(name, color, scale, opacity, - custom_image, customization) + name, color, scale, opacity, image = _replace_values(name, color, scale, opacity, + image, customization) if not isinstance(value, float): # other operators are only applicable to numbers @@ -92,8 +92,8 @@ def _get_icon_settings(state: dict, settings: ShowIconSettings) -> tuple[str, st or (operator == "<=" and value <= custom_icon_value) or (operator == ">" and value > custom_icon_value) or (operator == ">=" and value >= custom_icon_value)): - name, color, scale, opacity, custom_image = _replace_values(name, color, scale, opacity, - custom_image, customization) + name, color, scale, opacity, image = _replace_values(name, color, scale, opacity, + image, customization) # # End custom icon @@ -101,7 +101,7 @@ def _get_icon_settings(state: dict, settings: ShowIconSettings) -> tuple[str, st scale = round(scale / 100, 2) opacity = round(opacity / 100, 2) - return name, color, scale, opacity, custom_image + return name, color, scale, opacity, image def get_value(state: dict, customization: IconCustomization): @@ -116,13 +116,13 @@ def get_value(state: dict, customization: IconCustomization): return value -def _replace_values(name: str, color: str, scale: float, opacity: str, custom_image: str, +def _replace_values(name: str, color: str, scale: float, opacity: str, image: str, customization: IconCustomization): ret_name = name ret_color = color ret_scale = scale ret_opacity = opacity - ret_custom_image = custom_image + ret_image = image if customization.get_icon() is not None: ret_name = customization.get_icon() @@ -136,10 +136,10 @@ def _replace_values(name: str, color: str, scale: float, opacity: str, custom_im if customization.get_opacity() is not None: ret_opacity = customization.get_opacity() - if customization.get_custom_image() is not None: - ret_custom_image = customization.get_custom_image() + if customization.get_image() is not None: + ret_image = customization.get_image() - return ret_name, ret_color, ret_scale, ret_opacity, ret_custom_image + return ret_name, ret_color, ret_scale, ret_opacity, ret_image def _get_icon_path(name: str) -> str: diff --git a/actions/show_icon/icon_row.py b/actions/show_icon/icon_row.py index 3b42b24..b1262c4 100644 --- a/actions/show_icon/icon_row.py +++ b/actions/show_icon/icon_row.py @@ -38,4 +38,8 @@ def __init__(self, lm, customization: IconCustomization, customization_count: in title += (f"\n{self.lm.get(icon_const.LABEL_ICON_OPACITY)} " f"{int(customization.get_opacity())}") + if customization.get_image() is not None: + title += (f"\n{self.lm.get(icon_const.LABEL_ICON_IMAGE)} " + f"{customization.get_image()}") + self.set_title(title) diff --git a/actions/show_icon/icon_settings.py b/actions/show_icon/icon_settings.py index 2faa4b9..c9a404e 100644 --- a/actions/show_icon/icon_settings.py +++ b/actions/show_icon/icon_settings.py @@ -10,7 +10,7 @@ icon_const.SETTING_COLOR: icon_const.DEFAULT_ICON_COLOR, icon_const.SETTING_SCALE: icon_const.DEFAULT_ICON_SCALE, icon_const.SETTING_OPACITY: icon_const.DEFAULT_ICON_OPACITY, - icon_const.CUSTOM_IMAGE: icon_const.EMPTY_STRING, + icon_const.SETTING_IMAGE: icon_const.EMPTY_STRING, customization_const.SETTING_CUSTOMIZATIONS: [] } @@ -67,9 +67,9 @@ def get_opacity(self) -> int: """ return int(self._action.get_settings()[icon_const.SETTING_ICON][icon_const.SETTING_OPACITY]) - def get_custom_image(self) -> str: + def get_image(self) -> str: """ - Get the custom image path. - :return: the custom image path + Get the image path. + :return: the image path """ - return self._action.get_settings()[icon_const.SETTING_ICON].get(icon_const.CUSTOM_IMAGE, icon_const.EMPTY_STRING) + return self._action.get_settings()[icon_const.SETTING_ICON][icon_const.SETTING_IMAGE] diff --git a/actions/show_icon/icon_window.py b/actions/show_icon/icon_window.py index 8539783..98bc4b1 100644 --- a/actions/show_icon/icon_window.py +++ b/actions/show_icon/icon_window.py @@ -7,6 +7,7 @@ from typing import Callable import gi + gi.require_version("Gtk", "4.0") from gi.repository import Gio from gi.repository.Gtk import Box, Button, FileDialog, FileFilter, Label, Orientation @@ -167,8 +168,8 @@ def set_current_values(self) -> None: self.opacity_entry.set_text( str(int(self.current.get_opacity() or icon_const.DEFAULT_ICON_OPACITY))) - self.image_entry.set_text(self.current.get_custom_image() or icon_const.EMPTY_STRING) - self.check_image.set_active(self.current.get_custom_image() is not None) + self.image_entry.set_text(self.current.get_image() or icon_const.EMPTY_STRING) + self.check_image.set_active(self.current.get_image() is not None) self._on_check_image_toggled() def on_add_button(self, *_, **__) -> None: @@ -205,13 +206,13 @@ def on_add_button(self, *_, **__) -> None: scale = int(self.scale.get_value()) if self.check_scale.get_active() else None opacity = int( self.opacity.get_value()) if self.check_opacity.get_active() else None - custom_image = self.image_entry.get_text() if self.check_image.get_active() else None + image = self.image_entry.get_text() if self.check_image.get_active() else None self.callback(customization=IconCustomization( attribute=self.condition_attribute.get_selected_item().get_string(), operator=self.operator.get_selected_item().value, value=self.entry_value.get_text(), icon=icon, color=color_list, scale=scale, - opacity=opacity, custom_image=custom_image), index=self.index) + opacity=opacity, image=image), index=self.index) self.destroy() From ddf3725875968ef87ef602702d0de0548e69ddab Mon Sep 17 00:00:00 2001 From: David F Date: Fri, 15 May 2026 00:38:51 +0200 Subject: [PATCH 04/16] Merge image and icon field into one Instead of a separate image field, the icon field now accepts both MDI icon names and file paths. If the entered value is a valid MDI icon it is rendered as before, otherwise it is treated as an image/GIF path. Scale works for both. The filter customizations follow the same logic. --- actions/show_icon/icon_action.py | 37 +++++------ actions/show_icon/icon_const.py | 4 +- actions/show_icon/icon_customization.py | 6 +- actions/show_icon/icon_helper.py | 18 ++++-- actions/show_icon/icon_settings.py | 7 -- actions/show_icon/icon_window.py | 85 +++++++------------------ locales/de_DE.json | 2 +- locales/en_US.json | 2 +- 8 files changed, 58 insertions(+), 103 deletions(-) diff --git a/actions/show_icon/icon_action.py b/actions/show_icon/icon_action.py index 3307479..78ab59d 100644 --- a/actions/show_icon/icon_action.py +++ b/actions/show_icon/icon_action.py @@ -32,7 +32,6 @@ def __init__(self, *args, **kwargs): self.color = None self.scale = None self.opacity = None - self.image = None super().__init__(window_implementation=IconWindow, customization_implementation=IconCustomization, row_implementation=IconRow, settings_implementation=ShowIconSettings, track_entity=True, *args, **kwargs) @@ -40,7 +39,7 @@ def __init__(self, *args, **kwargs): def get_config_rows(self) -> list: """Get the rows to be displayed in the UI.""" return [self.domain_combo.widget, self.entity_combo.widget, self.icon.widget, self.color.widget, - self.scale.widget, self.opacity.widget, self.image.widget, self.customization_expander.widget] + self.scale.widget, self.opacity.widget, self.customization_expander.widget] def create_ui_elements(self) -> None: """Get all action rows.""" @@ -52,6 +51,11 @@ def create_ui_elements(self) -> None: complex_var_name=True ) + browse_button = Button(label=self.lm.get(icon_const.LABEL_ICON_BROWSE)) + browse_button.set_valign(Align.CENTER) + browse_button.connect("clicked", self._on_browse_clicked) + self.icon.widget.add_suffix(browse_button) + self.color: ColorButtonRow = ColorButtonRow( self, icon_const.SETTING_ICON_COLOR, icon_const.DEFAULT_ICON_COLOR, title=icon_const.LABEL_ICON_COLOR, on_change=self._reload, @@ -72,16 +76,6 @@ def create_ui_elements(self) -> None: can_reset=False, complex_var_name=True ) - self.image: EntryRow = EntryRow( - self, icon_const.SETTING_ICON_IMAGE, icon_const.EMPTY_STRING, - title=icon_const.LABEL_ICON_IMAGE, on_change=self._reload, - can_reset=False, complex_var_name=True - ) - - browse_button = Button(label=self.lm.get(icon_const.LABEL_ICON_BROWSE)) - browse_button.set_valign(Align.CENTER) - browse_button.connect("clicked", self._on_browse_clicked) - self.image.widget.add_suffix(browse_button) @requires_initialization def set_enabled_disabled(self) -> None: @@ -108,22 +102,21 @@ def set_enabled_disabled(self) -> None: self.opacity.widget.set_sensitive(False) self.opacity.widget.set_subtitle(self.lm.get(icon_const.LABEL_ICON_NO_ENTITY)) - self.image.widget.set_sensitive(False) else: - self.image.widget.set_sensitive(True) - - has_image = bool(self.settings.get_image()) + icon_value = self.settings.get_icon() + has_image = bool(icon_value) and icon_value not in icon_helper.MDI_ICONS + not_supported = self.lm.get(icon_const.LABEL_ICON_NOT_SUPPORTED_FOR_IMAGES) if has_image else icon_const.EMPTY_STRING - self.icon.widget.set_sensitive(not has_image) + self.icon.widget.set_sensitive(True) self.color.widget.set_sensitive(not has_image) - self.color.widget.set_subtitle(icon_const.EMPTY_STRING) + self.color.widget.set_subtitle(not_supported) - self.scale.widget.set_sensitive(not has_image) + self.scale.widget.set_sensitive(True) self.scale.widget.set_subtitle(icon_const.EMPTY_STRING) self.opacity.widget.set_sensitive(not has_image) - self.opacity.widget.set_subtitle(icon_const.EMPTY_STRING) + self.opacity.widget.set_subtitle(not_supported) def refresh(self, state: dict = None) -> None: """ @@ -163,7 +156,7 @@ def _on_browse_clicked(self, *_) -> None: dialog.set_filters(filters) dialog.set_default_filter(filter_images) - current_path = self.image.widget.get_text() + current_path = self.icon.widget.get_text() if current_path: current_dir = os.path.dirname(current_path) if os.path.isdir(current_dir): @@ -175,7 +168,7 @@ def _on_file_chosen(self, dialog, result) -> None: try: file = dialog.open_finish(result) if file: - self.image.widget.set_text(file.get_path()) + self.icon.widget.set_text(file.get_path()) self._reload() except Exception: pass diff --git a/actions/show_icon/icon_const.py b/actions/show_icon/icon_const.py index 2f9f70d..6f18b1a 100644 --- a/actions/show_icon/icon_const.py +++ b/actions/show_icon/icon_const.py @@ -13,12 +13,10 @@ SETTING_COLOR = "color" SETTING_SCALE = "scale" SETTING_OPACITY = "opacity" -SETTING_IMAGE = "image" SETTING_ICON_ICON = f"{SETTING_ICON}.{SETTING_ICON}" SETTING_ICON_COLOR = f"{SETTING_ICON}.{SETTING_COLOR}" SETTING_ICON_SCALE = f"{SETTING_ICON}.{SETTING_SCALE}" SETTING_ICON_OPACITY = f"{SETTING_ICON}.{SETTING_OPACITY}" -SETTING_ICON_IMAGE = f"{SETTING_ICON}.{SETTING_IMAGE}" DEFAULT_ICON_COLOR = [237, 255, 26, 255] DEFAULT_ICON_SCALE = 80 @@ -36,8 +34,8 @@ CUSTOM_IMAGE = "image" LABEL_ICON_IMAGE = "actions.home_assistant.icon.image.label" -LABEL_ICON_IMAGE_OVERRIDES = "actions.home_assistant.icon.image_overrides.label" LABEL_ICON_BROWSE = "actions.home_assistant.icon.browse.label" +LABEL_ICON_NOT_SUPPORTED_FOR_IMAGES = "actions.home_assistant.icon.not_supported_for_images.label" MDI_SVG_JSON = "assets/mdi-svg.json" ICON_COLOR_RED = "#ff0000" diff --git a/actions/show_icon/icon_customization.py b/actions/show_icon/icon_customization.py index d4d0938..7d99555 100644 --- a/actions/show_icon/icon_customization.py +++ b/actions/show_icon/icon_customization.py @@ -28,9 +28,9 @@ def from_dict(cls, customization: dict): return cls(customization[customization_const.CONDITION][customization_const.ATTRIBUTE], customization[customization_const.CONDITION][customization_const.OPERATOR], customization[customization_const.CONDITION][customization_const.VALUE], - customization[icon_const.CUSTOM_ICON], customization[icon_const.CUSTOM_COLOR], - customization[icon_const.CUSTOM_SCALE], customization[icon_const.CUSTOM_OPACITY], - customization[icon_const.CUSTOM_IMAGE]) + customization.get(icon_const.CUSTOM_ICON), customization.get(icon_const.CUSTOM_COLOR), + customization.get(icon_const.CUSTOM_SCALE), customization.get(icon_const.CUSTOM_OPACITY), + customization.get(icon_const.CUSTOM_IMAGE)) def get_icon(self) -> str: """ diff --git a/actions/show_icon/icon_helper.py b/actions/show_icon/icon_helper.py index 7fae403..fa3148c 100644 --- a/actions/show_icon/icon_helper.py +++ b/actions/show_icon/icon_helper.py @@ -46,10 +46,13 @@ def _get_icon_settings(state: dict, settings: ShowIconSettings) -> tuple[str, st color = settings.get_color() scale = settings.get_scale() opacity = settings.get_opacity() - image = settings.get_image() or None + image = None - if settings.get_icon() in MDI_ICONS.keys(): - name = settings.get_icon() + icon_value = settings.get_icon() + if icon_value in MDI_ICONS: + name = icon_value + elif icon_value: + image = icon_value # # Begin custom icon @@ -125,7 +128,14 @@ def _replace_values(name: str, color: str, scale: float, opacity: str, image: st ret_image = image if customization.get_icon() is not None: - ret_name = customization.get_icon() + icon_val = customization.get_icon() + if icon_val in MDI_ICONS: + ret_name = icon_val + else: + ret_image = icon_val + + if customization.get_image() is not None: + ret_image = customization.get_image() if customization.get_color() is not None: ret_color = customization.get_color() diff --git a/actions/show_icon/icon_settings.py b/actions/show_icon/icon_settings.py index c9a404e..2d178c5 100644 --- a/actions/show_icon/icon_settings.py +++ b/actions/show_icon/icon_settings.py @@ -10,7 +10,6 @@ icon_const.SETTING_COLOR: icon_const.DEFAULT_ICON_COLOR, icon_const.SETTING_SCALE: icon_const.DEFAULT_ICON_SCALE, icon_const.SETTING_OPACITY: icon_const.DEFAULT_ICON_OPACITY, - icon_const.SETTING_IMAGE: icon_const.EMPTY_STRING, customization_const.SETTING_CUSTOMIZATIONS: [] } @@ -67,9 +66,3 @@ def get_opacity(self) -> int: """ return int(self._action.get_settings()[icon_const.SETTING_ICON][icon_const.SETTING_OPACITY]) - def get_image(self) -> str: - """ - Get the image path. - :return: the image path - """ - return self._action.get_settings()[icon_const.SETTING_ICON][icon_const.SETTING_IMAGE] diff --git a/actions/show_icon/icon_window.py b/actions/show_icon/icon_window.py index 98bc4b1..fa60066 100644 --- a/actions/show_icon/icon_window.py +++ b/actions/show_icon/icon_window.py @@ -7,10 +7,9 @@ from typing import Callable import gi - gi.require_version("Gtk", "4.0") from gi.repository import Gio -from gi.repository.Gtk import Box, Button, FileDialog, FileFilter, Label, Orientation +from gi.repository.Gtk import Button, FileDialog, FileFilter from HomeAssistantPlugin.actions import const as base_const from HomeAssistantPlugin.actions.cores.customization_core import customization_helper @@ -36,22 +35,18 @@ def __init__(self, lm, attributes: list, callback: Callable, self.check_color = self._create_check_button() self.check_scale = self._create_check_button() self.check_opacity = self._create_check_button() - self.check_image = self._create_check_button() label_icon = self._create_label(self.lm.get(icon_const.LABEL_ICON_ICON)) label_color = self._create_label(self.lm.get(icon_const.LABEL_ICON_COLOR)) label_scale = self._create_label(self.lm.get(icon_const.LABEL_ICON_SCALE)) label_opacity = self._create_label(self.lm.get(icon_const.LABEL_ICON_OPACITY)) - label_image = self._create_label(self.lm.get(icon_const.LABEL_ICON_IMAGE)) self.icon = self._create_entry(self.check_icon) self.icon.set_margin_end(self.default_margin) self.connect_rows.append( partial(self.icon.connect, base_const.CONNECT_ACTIVATE, self.on_widget_changed)) - - self.image_entry = self._create_entry(self.check_image) - self.image_entry.set_hexpand(True) - self.image_entry.set_margin_end(self.default_margin) + self.connect_rows.append( + partial(self.icon.connect, base_const.CONNECT_CHANGED, self._on_icon_changed)) browse_button = Button(label=self.lm.get(icon_const.LABEL_ICON_BROWSE)) browse_button.set_margin_top(self.default_margin) @@ -59,20 +54,6 @@ def __init__(self, lm, attributes: list, callback: Callable, browse_button.set_margin_start(self.default_margin) browse_button.set_margin_end(self.default_margin) self.connect_rows.append(partial(browse_button.connect, base_const.CONNECT_CLICKED, self._on_browse_clicked)) - self.connect_rows.append(partial(self.check_image.connect, "toggled", self._on_check_image_toggled)) - - image_box = Box() - image_box.append(self.image_entry) - image_box.append(browse_button) - - image_hint_label = Label(label=self.lm.get(icon_const.LABEL_ICON_IMAGE_OVERRIDES)) - image_hint_label.add_css_class("caption") - image_hint_label.add_css_class("dim-label") - image_hint_label.set_xalign(0) - - image_col_box = Box(orientation=Orientation.VERTICAL) - image_col_box.append(image_box) - image_col_box.append(image_hint_label) self.color = self._create_color_button(self.check_color) @@ -109,6 +90,7 @@ def __init__(self, lm, attributes: list, callback: Callable, self.grid_fields.attach(self.check_icon, 2, 2, 1, 1) self.grid_fields.attach(label_icon, 3, 2, 1, 1) self.grid_fields.attach(self.icon, 4, 2, 1, 1) + self.grid_fields.attach(browse_button, 5, 2, 1, 1) self.grid_fields.attach(self.check_color, 2, 3, 1, 1) self.grid_fields.attach(label_color, 3, 3, 1, 1) @@ -124,10 +106,6 @@ def __init__(self, lm, attributes: list, callback: Callable, self.grid_fields.attach(self.opacity, 4, 5, 1, 1) self.grid_fields.attach(self.opacity_entry, 5, 5, 1, 1) - self.grid_fields.attach(self.check_image, 2, 6, 1, 1) - self.grid_fields.attach(label_image, 3, 6, 1, 1) - self.grid_fields.attach(image_col_box, 4, 6, 2, 1) - self._after_init() def set_default_values(self) -> None: @@ -148,8 +126,11 @@ def set_current_values(self) -> None: super().set_current_values() - self.icon.set_text(self.current.get_icon() or icon_const.EMPTY_STRING) - self.check_icon.set_active(self.current.get_icon() is not None) + # backward compat: old customizations may have stored the path in image instead of icon + icon_or_image = self.current.get_icon() or self.current.get_image() + self.icon.set_text(icon_or_image or icon_const.EMPTY_STRING) + self.check_icon.set_active(icon_or_image is not None) + self._on_icon_changed() if self.current.get_color(): rgba = customization_helper.convert_color_list_to_rgba(self.current.get_color()) @@ -168,59 +149,40 @@ def set_current_values(self) -> None: self.opacity_entry.set_text( str(int(self.current.get_opacity() or icon_const.DEFAULT_ICON_OPACITY))) - self.image_entry.set_text(self.current.get_image() or icon_const.EMPTY_STRING) - self.check_image.set_active(self.current.get_image() is not None) - self._on_check_image_toggled() - def on_add_button(self, *_, **__) -> None: if not super().on_add_button(): return - if self.check_icon.get_active(): - icon = self.icon.get_text() - - if icon.startswith("mdi:"): - icon = icon[4:] - - if icon not in self.icons: - self.icon.add_css_class(icon_const.ERROR) - return - - if self.check_image.get_active() and not self.image_entry.get_text(): - self.image_entry.add_css_class(icon_const.ERROR) + if self.check_icon.get_active() and not self.icon.get_text(): + self.icon.add_css_class(icon_const.ERROR) return if (not self.check_icon.get_active() and not self.check_color.get_active() and not - self.check_scale.get_active() and not self.check_opacity.get_active() and not - self.check_image.get_active()): + self.check_scale.get_active() and not self.check_opacity.get_active()): self.check_icon.add_css_class(icon_const.ERROR) self.check_color.add_css_class(icon_const.ERROR) self.check_scale.add_css_class(icon_const.ERROR) self.check_opacity.add_css_class(icon_const.ERROR) - self.check_image.add_css_class(icon_const.ERROR) return icon = self.icon.get_text() if self.check_icon.get_active() else None color = self.color.get_rgba() if self.check_color.get_active() else None color_list = customization_helper.convert_rgba_to_color_list(color) if color else None scale = int(self.scale.get_value()) if self.check_scale.get_active() else None - opacity = int( - self.opacity.get_value()) if self.check_opacity.get_active() else None - image = self.image_entry.get_text() if self.check_image.get_active() else None + opacity = int(self.opacity.get_value()) if self.check_opacity.get_active() else None self.callback(customization=IconCustomization( attribute=self.condition_attribute.get_selected_item().get_string(), operator=self.operator.get_selected_item().value, value=self.entry_value.get_text(), icon=icon, color=color_list, scale=scale, - opacity=opacity, image=image), index=self.index) + opacity=opacity, image=None), index=self.index) self.destroy() - def _on_check_image_toggled(self, *_) -> None: - has_image = self.check_image.get_active() - for widget in [self.check_icon, self.icon, self.check_color, self.color, - self.check_scale, self.scale, self.scale_entry, - self.check_opacity, self.opacity, self.opacity_entry]: + def _on_icon_changed(self, *_) -> None: + icon_value = self.icon.get_text() + has_image = bool(icon_value) and icon_value not in icon_helper.MDI_ICONS + for widget in [self.check_color, self.color, self.check_opacity, self.opacity, self.opacity_entry]: widget.set_sensitive(not has_image) def _on_browse_clicked(self, *_) -> None: @@ -237,8 +199,8 @@ def _on_browse_clicked(self, *_) -> None: dialog.set_filters(filters) dialog.set_default_filter(filter_images) - if self.image_entry.get_text(): - current_dir = os.path.dirname(self.image_entry.get_text()) + if self.icon.get_text(): + current_dir = os.path.dirname(self.icon.get_text()) if os.path.isdir(current_dir): dialog.set_initial_folder(Gio.File.new_for_path(current_dir)) @@ -248,8 +210,9 @@ def _on_file_chosen(self, dialog, result) -> None: try: file = dialog.open_finish(result) if file: - self.image_entry.set_text(file.get_path()) - self.check_image.set_active(True) + self.icon.set_text(file.get_path()) + self.check_icon.set_active(True) + self._on_icon_changed() except Exception: pass @@ -261,5 +224,3 @@ def on_widget_changed(self, *_, **__) -> None: self.check_color.remove_css_class(icon_const.ERROR) self.check_scale.remove_css_class(icon_const.ERROR) self.check_opacity.remove_css_class(icon_const.ERROR) - self.image_entry.remove_css_class(icon_const.ERROR) - self.check_image.remove_css_class(icon_const.ERROR) diff --git a/locales/de_DE.json b/locales/de_DE.json index a7b283e..2d3a65c 100644 --- a/locales/de_DE.json +++ b/locales/de_DE.json @@ -29,8 +29,8 @@ "actions.home_assistant.icon.scale.label": "Größe:", "actions.home_assistant.icon.custom_icon.label": "Anpassen:", "actions.home_assistant.icon.image.label": "Bild:", - "actions.home_assistant.icon.image_overrides.label": "Überschreibt Icon, Farbe, Größe und Deckkraft", "actions.home_assistant.icon.browse.label": "Durchsuchen...", + "actions.home_assistant.icon.not_supported_for_images.label": "Wird für Bilder nicht unterstützt", "actions.home_assistant.icon.no_entity.label": "Keine Entität ausgewählt", "actions.home_assistant.text.show_text.label": "Text anzeigen:", diff --git a/locales/en_US.json b/locales/en_US.json index 8fcddb2..fb6e7fc 100644 --- a/locales/en_US.json +++ b/locales/en_US.json @@ -29,8 +29,8 @@ "actions.home_assistant.icon.scale.label": "Scale:", "actions.home_assistant.icon.custom_icon.label": "Customize:", "actions.home_assistant.icon.image.label": "Image:", - "actions.home_assistant.icon.image_overrides.label": "Overrides icon, color, scale and opacity", "actions.home_assistant.icon.browse.label": "Browse...", + "actions.home_assistant.icon.not_supported_for_images.label": "Not supported for images", "actions.home_assistant.icon.no_entity.label": "No entity selected", "actions.home_assistant.text.show_text.label": "Show text:", From efff30d3d02f9ee7ee8da73ce4a423f770e12e93 Mon Sep 17 00:00:00 2001 From: gensyn Date: Sun, 17 May 2026 22:11:09 +0200 Subject: [PATCH 05/16] Cleaned up code for image support --- actions/const.py | 1 + actions/cores/base_core/base_core.py | 4 +- actions/cores/base_core/base_settings.py | 1 + actions/cores/base_core/migrate.py | 23 ++++++------ actions/show_icon/icon_action.py | 14 ++++--- actions/show_icon/icon_const.py | 2 +- actions/show_icon/icon_customization.py | 22 +++-------- actions/show_icon/icon_helper.py | 47 +++++++----------------- actions/show_icon/icon_row.py | 4 -- actions/show_icon/icon_settings.py | 17 ++------- actions/show_icon/icon_window.py | 18 ++++----- locales/de_DE.json | 2 +- locales/en_US.json | 4 +- 13 files changed, 59 insertions(+), 100 deletions(-) diff --git a/actions/const.py b/actions/const.py index 72ffb2b..e087bab 100644 --- a/actions/const.py +++ b/actions/const.py @@ -15,6 +15,7 @@ LABEL_NO_ENTITY = "actions.home_assistant.customization.no_entity.label" SETTING_VERSION = "version" +SETTING_VERSION_NUMBER = 1 SETTING_ENTITY = "entity" SETTING_DOMAIN = "domain" diff --git a/actions/cores/base_core/base_core.py b/actions/cores/base_core/base_core.py index d0b54e5..5c6a421 100644 --- a/actions/cores/base_core/base_core.py +++ b/actions/cores/base_core/base_core.py @@ -2,7 +2,7 @@ import gi from HomeAssistantPlugin.actions import const -from HomeAssistantPlugin.actions.cores.base_core.migrate import migrate +from HomeAssistantPlugin.actions.cores.base_core.migrate import migrate_settings from GtkHelper.GenerativeUI.ComboRow import ComboRow from src.backend.PluginManager.ActionCore import ActionCore @@ -50,7 +50,7 @@ def __init__(self, settings_implementation, track_entity: bool, *args, **kwargs) def on_ready(self) -> None: """Set up action when StreamController has finished loading.""" - migrate(self) + migrate_settings(self) self.settings = self.settings_implementation(self) self.initialized = True diff --git a/actions/cores/base_core/base_settings.py b/actions/cores/base_core/base_settings.py index 2755b24..62eb14e 100644 --- a/actions/cores/base_core/base_settings.py +++ b/actions/cores/base_core/base_settings.py @@ -3,6 +3,7 @@ from HomeAssistantPlugin.actions import const DEFAULT_SETTINGS = { + const.SETTING_VERSION: const.SETTING_VERSION_NUMBER, const.SETTING_DOMAIN: const.EMPTY_STRING, const.SETTING_ENTITY: const.EMPTY_STRING } diff --git a/actions/cores/base_core/migrate.py b/actions/cores/base_core/migrate.py index ddf5b25..3088210 100644 --- a/actions/cores/base_core/migrate.py +++ b/actions/cores/base_core/migrate.py @@ -4,25 +4,24 @@ from HomeAssistantPlugin.actions.show_icon.icon_settings import ShowIconSettings -def migrate(action) -> None: +def migrate_settings(action) -> None: settings = action.get_settings() - if settings.get(const.SETTING_VERSION, 0) > 1: + if settings.get(const.SETTING_VERSION) is not None: return - migrate_v1_v2(action) + migrate_v0_v1(action) -def migrate_v1_v2(action) -> None: +def migrate_v0_v1(action) -> None: + """ + Migrate from v1 to v2. + There is actually nothing to do yet - this just introduces the version field. + """ settings = action.get_settings() - settings[const.SETTING_VERSION] = 2 - action.set_settings(settings) - - if action.__class__.__name__ != "ShowIcon": + if not settings: + # settings are empty - this is a brand-new action return - settings[icon_const.SETTING_ICON][icon_const.SETTING_IMAGE] = icon_const.EMPTY_STRING - for customization in settings[icon_const.SETTING_ICON][customization_const.SETTING_CUSTOMIZATIONS]: - customization[icon_const.CUSTOM_IMAGE] = icon_const.EMPTY_STRING - + settings[const.SETTING_VERSION] = const.SETTING_VERSION_NUMBER action.set_settings(settings) \ No newline at end of file diff --git a/actions/show_icon/icon_action.py b/actions/show_icon/icon_action.py index 78ab59d..c715d83 100644 --- a/actions/show_icon/icon_action.py +++ b/actions/show_icon/icon_action.py @@ -10,6 +10,8 @@ from gi.repository import Gio from gi.repository.Gtk import Align, Button, FileDialog, FileFilter +from loguru import logger as log + from HomeAssistantPlugin.actions.cores.base_core.base_core import requires_initialization from HomeAssistantPlugin.actions.cores.customization_core.customization_core import CustomizationCore from HomeAssistantPlugin.actions.show_icon import icon_const, icon_helper @@ -104,18 +106,18 @@ def set_enabled_disabled(self) -> None: else: icon_value = self.settings.get_icon() - has_image = bool(icon_value) and icon_value not in icon_helper.MDI_ICONS - not_supported = self.lm.get(icon_const.LABEL_ICON_NOT_SUPPORTED_FOR_IMAGES) if has_image else icon_const.EMPTY_STRING + has_icon = bool(icon_value) and icon_value in icon_helper.MDI_ICONS + not_supported = self.lm.get(icon_const.LABEL_ICON_ONLY_SUPPORTED_FOR_ICONS) if not has_icon else icon_const.EMPTY_STRING self.icon.widget.set_sensitive(True) - self.color.widget.set_sensitive(not has_image) + self.color.widget.set_sensitive(has_icon) self.color.widget.set_subtitle(not_supported) self.scale.widget.set_sensitive(True) self.scale.widget.set_subtitle(icon_const.EMPTY_STRING) - self.opacity.widget.set_sensitive(not has_image) + self.opacity.widget.set_sensitive(has_icon) self.opacity.widget.set_subtitle(not_supported) def refresh(self, state: dict = None) -> None: @@ -170,8 +172,8 @@ def _on_file_chosen(self, dialog, result) -> None: if file: self.icon.widget.set_text(file.get_path()) self._reload() - except Exception: - pass + except Exception as e: + log.error(f"Error choosing file: {e}") def _get_domains(self) -> list[str]: """This class needs all domains that provide actions in Home Assistant.""" diff --git a/actions/show_icon/icon_const.py b/actions/show_icon/icon_const.py index 6f18b1a..cdbd391 100644 --- a/actions/show_icon/icon_const.py +++ b/actions/show_icon/icon_const.py @@ -35,7 +35,7 @@ LABEL_ICON_IMAGE = "actions.home_assistant.icon.image.label" LABEL_ICON_BROWSE = "actions.home_assistant.icon.browse.label" -LABEL_ICON_NOT_SUPPORTED_FOR_IMAGES = "actions.home_assistant.icon.not_supported_for_images.label" +LABEL_ICON_ONLY_SUPPORTED_FOR_ICONS = "actions.home_assistant.icon.only_supported_for_icons.label" MDI_SVG_JSON = "assets/mdi-svg.json" ICON_COLOR_RED = "#ff0000" diff --git a/actions/show_icon/icon_customization.py b/actions/show_icon/icon_customization.py index 7d99555..606b0c4 100644 --- a/actions/show_icon/icon_customization.py +++ b/actions/show_icon/icon_customization.py @@ -1,5 +1,5 @@ """ -Module to manage icon customizations. +Modul to manage icon customizations. """ from typing import Any @@ -15,22 +15,20 @@ class IconCustomization(Customization): """ def __init__(self, attribute: str, operator: str, value: str, icon: str, color: tuple[int, int, int, int], - scale: int, opacity: int, image: str): + scale: int, opacity: int): super().__init__(attribute, operator, value) self.icon: str = icon self.color: tuple[int, int, int, int] = color self.scale: int = scale self.opacity: int = opacity - self.image: str = image @classmethod def from_dict(cls, customization: dict): return cls(customization[customization_const.CONDITION][customization_const.ATTRIBUTE], customization[customization_const.CONDITION][customization_const.OPERATOR], customization[customization_const.CONDITION][customization_const.VALUE], - customization.get(icon_const.CUSTOM_ICON), customization.get(icon_const.CUSTOM_COLOR), - customization.get(icon_const.CUSTOM_SCALE), customization.get(icon_const.CUSTOM_OPACITY), - customization.get(icon_const.CUSTOM_IMAGE)) + customization[icon_const.CUSTOM_ICON], customization[icon_const.CUSTOM_COLOR], + customization[icon_const.CUSTOM_SCALE], customization[icon_const.CUSTOM_OPACITY]) def get_icon(self) -> str: """ @@ -60,13 +58,6 @@ def get_opacity(self) -> int: """ return self.opacity - def get_image(self) -> str: - """ - Get the image path. - :return: path to an image/GIF file, or None - """ - return self.image - def export(self) -> dict[str, Any]: """ Get this customization as a dict. @@ -81,6 +72,5 @@ def export(self) -> dict[str, Any]: icon_const.CUSTOM_ICON: self.icon, icon_const.CUSTOM_COLOR: self.color, icon_const.CUSTOM_SCALE: self.scale, - icon_const.CUSTOM_OPACITY: self.opacity, - icon_const.CUSTOM_IMAGE: self.image - } + icon_const.CUSTOM_OPACITY: self.opacity + } \ No newline at end of file diff --git a/actions/show_icon/icon_helper.py b/actions/show_icon/icon_helper.py index fa3148c..8ed5c3e 100644 --- a/actions/show_icon/icon_helper.py +++ b/actions/show_icon/icon_helper.py @@ -27,10 +27,11 @@ def get_icon(state: dict, settings: ShowIconSettings, is_connected: bool) -> tup icon_const.ICON_COLOR_RED).replace( "", "1.0")), round(icon_const.DEFAULT_ICON_SCALE / 100, 2) - name, color, scale, opacity, image = _get_icon_settings(state, settings) + name, color, scale, opacity = _get_icon_settings(state, settings) - if image: - return image, scale + if name not in MDI_ICONS: + # color and opacity only have an effect on icons + return name, scale # convert RGB color to hex color = customization_helper.convert_color_list_to_hex(color) @@ -40,19 +41,12 @@ def get_icon(state: dict, settings: ShowIconSettings, is_connected: bool) -> tup return (icon.replace("", color).replace("", str(opacity))), scale -def _get_icon_settings(state: dict, settings: ShowIconSettings) -> tuple[str, str, float, float, str]: +def _get_icon_settings(state: dict, settings: ShowIconSettings) -> tuple[str, str, float, float]: # default value for the icon is the icon set in HA - name = state.get(icon_const.ATTRIBUTES, {}).get(icon_const.ATTRIBUTE_ICON, icon_const.EMPTY_STRING) + name = state.get(icon_const.ATTRIBUTES, {}).get(icon_const.ATTRIBUTE_ICON, icon_const.EMPTY_STRING) or settings.get_icon() color = settings.get_color() scale = settings.get_scale() opacity = settings.get_opacity() - image = None - - icon_value = settings.get_icon() - if icon_value in MDI_ICONS: - name = icon_value - elif icon_value: - image = icon_value # # Begin custom icon @@ -77,8 +71,8 @@ def _get_icon_settings(state: dict, settings: ShowIconSettings) -> tuple[str, st if ((operator == "==" and str(value) == str(custom_icon_value)) or (operator == "!=" and str(value) != str(custom_icon_value))): - name, color, scale, opacity, image = _replace_values(name, color, scale, opacity, - image, customization) + name, color, scale, opacity = _replace_values(name, color, scale, opacity, + customization) if not isinstance(value, float): # other operators are only applicable to numbers @@ -95,8 +89,7 @@ def _get_icon_settings(state: dict, settings: ShowIconSettings) -> tuple[str, st or (operator == "<=" and value <= custom_icon_value) or (operator == ">" and value > custom_icon_value) or (operator == ">=" and value >= custom_icon_value)): - name, color, scale, opacity, image = _replace_values(name, color, scale, opacity, - image, customization) + name, color, scale, opacity = _replace_values(name, color, scale, opacity, customization) # # End custom icon @@ -104,7 +97,7 @@ def _get_icon_settings(state: dict, settings: ShowIconSettings) -> tuple[str, st scale = round(scale / 100, 2) opacity = round(opacity / 100, 2) - return name, color, scale, opacity, image + return name, color, scale, opacity def get_value(state: dict, customization: IconCustomization): @@ -119,23 +112,14 @@ def get_value(state: dict, customization: IconCustomization): return value -def _replace_values(name: str, color: str, scale: float, opacity: str, image: str, - customization: IconCustomization): +def _replace_values(name: str, color: str, scale: float, opacity: str, customization: IconCustomization): ret_name = name ret_color = color ret_scale = scale ret_opacity = opacity - ret_image = image if customization.get_icon() is not None: - icon_val = customization.get_icon() - if icon_val in MDI_ICONS: - ret_name = icon_val - else: - ret_image = icon_val - - if customization.get_image() is not None: - ret_image = customization.get_image() + ret_name = customization.get_icon() if customization.get_color() is not None: ret_color = customization.get_color() @@ -146,10 +130,7 @@ def _replace_values(name: str, color: str, scale: float, opacity: str, image: st if customization.get_opacity() is not None: ret_opacity = customization.get_opacity() - if customization.get_image() is not None: - ret_image = customization.get_image() - - return ret_name, ret_color, ret_scale, ret_opacity, ret_image + return ret_name, ret_color, ret_scale, ret_opacity def _get_icon_path(name: str) -> str: @@ -173,4 +154,4 @@ def _get_icon_svg(name: str) -> str: return (f'{name}') + f'/>') \ No newline at end of file diff --git a/actions/show_icon/icon_row.py b/actions/show_icon/icon_row.py index b1262c4..3b42b24 100644 --- a/actions/show_icon/icon_row.py +++ b/actions/show_icon/icon_row.py @@ -38,8 +38,4 @@ def __init__(self, lm, customization: IconCustomization, customization_count: in title += (f"\n{self.lm.get(icon_const.LABEL_ICON_OPACITY)} " f"{int(customization.get_opacity())}") - if customization.get_image() is not None: - title += (f"\n{self.lm.get(icon_const.LABEL_ICON_IMAGE)} " - f"{customization.get_image()}") - self.set_title(title) diff --git a/actions/show_icon/icon_settings.py b/actions/show_icon/icon_settings.py index 2d178c5..523f5c0 100644 --- a/actions/show_icon/icon_settings.py +++ b/actions/show_icon/icon_settings.py @@ -23,20 +23,10 @@ class ShowIconSettings(CustomizationSettings): def __init__(self, action): super().__init__(action, icon_const.SETTING_ICON, IconCustomization) - settings = self._action.get_settings() - icon_settings = settings.get(icon_const.SETTING_ICON) - if not icon_settings: + if not self._action.get_settings().get(icon_const.SETTING_ICON): + settings = self._action.get_settings() settings[icon_const.SETTING_ICON] = DEFAULT_SETTINGS.copy() self._action.set_settings(settings) - else: - # Fill in any missing keys from DEFAULT_SETTINGS - changed = False - for key, value in DEFAULT_SETTINGS.items(): - if key not in icon_settings: - icon_settings[key] = value - changed = True - if changed: - self._action.set_settings(settings) def get_icon(self) -> str: """ @@ -64,5 +54,4 @@ def get_opacity(self) -> int: Get the opacity. :return: the opacity """ - return int(self._action.get_settings()[icon_const.SETTING_ICON][icon_const.SETTING_OPACITY]) - + return int(self._action.get_settings()[icon_const.SETTING_ICON][icon_const.SETTING_OPACITY]) \ No newline at end of file diff --git a/actions/show_icon/icon_window.py b/actions/show_icon/icon_window.py index fa60066..a86c90b 100644 --- a/actions/show_icon/icon_window.py +++ b/actions/show_icon/icon_window.py @@ -11,6 +11,8 @@ from gi.repository import Gio from gi.repository.Gtk import Button, FileDialog, FileFilter +from loguru import logger as log + from HomeAssistantPlugin.actions import const as base_const from HomeAssistantPlugin.actions.cores.customization_core import customization_helper from HomeAssistantPlugin.actions.cores.customization_core.customization_window import CustomizationWindow @@ -126,10 +128,8 @@ def set_current_values(self) -> None: super().set_current_values() - # backward compat: old customizations may have stored the path in image instead of icon - icon_or_image = self.current.get_icon() or self.current.get_image() - self.icon.set_text(icon_or_image or icon_const.EMPTY_STRING) - self.check_icon.set_active(icon_or_image is not None) + self.icon.set_text(self.current.get_icon() or icon_const.EMPTY_STRING) + self.check_icon.set_active(self.current.get_icon() is not None) self._on_icon_changed() if self.current.get_color(): @@ -175,15 +175,15 @@ def on_add_button(self, *_, **__) -> None: attribute=self.condition_attribute.get_selected_item().get_string(), operator=self.operator.get_selected_item().value, value=self.entry_value.get_text(), icon=icon, color=color_list, scale=scale, - opacity=opacity, image=None), index=self.index) + opacity=opacity), index=self.index) self.destroy() def _on_icon_changed(self, *_) -> None: icon_value = self.icon.get_text() - has_image = bool(icon_value) and icon_value not in icon_helper.MDI_ICONS + has_icon = bool(icon_value) and icon_value in icon_helper.MDI_ICONS for widget in [self.check_color, self.color, self.check_opacity, self.opacity, self.opacity_entry]: - widget.set_sensitive(not has_image) + widget.set_sensitive(has_icon) def _on_browse_clicked(self, *_) -> None: dialog = FileDialog() @@ -213,8 +213,8 @@ def _on_file_chosen(self, dialog, result) -> None: self.icon.set_text(file.get_path()) self.check_icon.set_active(True) self._on_icon_changed() - except Exception: - pass + except Exception as e: + log.error(f"Error choosing file: {e}") def on_widget_changed(self, *_, **__) -> None: super().on_widget_changed() diff --git a/locales/de_DE.json b/locales/de_DE.json index 2d3a65c..47ebac7 100644 --- a/locales/de_DE.json +++ b/locales/de_DE.json @@ -30,7 +30,7 @@ "actions.home_assistant.icon.custom_icon.label": "Anpassen:", "actions.home_assistant.icon.image.label": "Bild:", "actions.home_assistant.icon.browse.label": "Durchsuchen...", - "actions.home_assistant.icon.not_supported_for_images.label": "Wird für Bilder nicht unterstützt", + "actions.home_assistant.icon.only_supported_for_icons.label": "Nur für Icons unterstützt", "actions.home_assistant.icon.no_entity.label": "Keine Entität ausgewählt", "actions.home_assistant.text.show_text.label": "Text anzeigen:", diff --git a/locales/en_US.json b/locales/en_US.json index fb6e7fc..1ad2f41 100644 --- a/locales/en_US.json +++ b/locales/en_US.json @@ -28,9 +28,9 @@ "actions.home_assistant.icon.opacity.label": "Opacity:", "actions.home_assistant.icon.scale.label": "Scale:", "actions.home_assistant.icon.custom_icon.label": "Customize:", - "actions.home_assistant.icon.image.label": "Image:", "actions.home_assistant.icon.browse.label": "Browse...", - "actions.home_assistant.icon.not_supported_for_images.label": "Not supported for images", + "actions.home_assistant.icon.image.label": "Image:", + "actions.home_assistant.icon.only_supported_for_icons.label": "Only supported for icons", "actions.home_assistant.icon.no_entity.label": "No entity selected", "actions.home_assistant.text.show_text.label": "Show text:", From 423fbe7d78ca76351e57f205ff6cae5954a6cf4b Mon Sep 17 00:00:00 2001 From: gensyn Date: Mon, 18 May 2026 08:22:31 +0200 Subject: [PATCH 06/16] Fixed MDI string handling --- actions/show_icon/icon_helper.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/actions/show_icon/icon_helper.py b/actions/show_icon/icon_helper.py index 8ed5c3e..8d39c1c 100644 --- a/actions/show_icon/icon_helper.py +++ b/actions/show_icon/icon_helper.py @@ -43,7 +43,7 @@ def get_icon(state: dict, settings: ShowIconSettings, is_connected: bool) -> tup def _get_icon_settings(state: dict, settings: ShowIconSettings) -> tuple[str, str, float, float]: # default value for the icon is the icon set in HA - name = state.get(icon_const.ATTRIBUTES, {}).get(icon_const.ATTRIBUTE_ICON, icon_const.EMPTY_STRING) or settings.get_icon() + name = settings.get_icon() or state.get(icon_const.ATTRIBUTES, {}).get(icon_const.ATTRIBUTE_ICON, icon_const.EMPTY_STRING).replace("mdi:", icon_const.EMPTY_STRING) color = settings.get_color() scale = settings.get_scale() opacity = settings.get_opacity() From 8f0b725a8bc5bebda33d92b04658ceea1ab32519 Mon Sep 17 00:00:00 2001 From: gensyn Date: Mon, 18 May 2026 08:24:24 +0200 Subject: [PATCH 07/16] Fixed migration doc string. --- actions/cores/base_core/migrate.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/actions/cores/base_core/migrate.py b/actions/cores/base_core/migrate.py index 3088210..c122dfe 100644 --- a/actions/cores/base_core/migrate.py +++ b/actions/cores/base_core/migrate.py @@ -14,7 +14,7 @@ def migrate_settings(action) -> None: def migrate_v0_v1(action) -> None: """ - Migrate from v1 to v2. + Migrate from no version to v1. There is actually nothing to do yet - this just introduces the version field. """ settings = action.get_settings() From eaa81f6b7ee472cbb8624af20edecf71552281ec Mon Sep 17 00:00:00 2001 From: gensyn Date: Mon, 18 May 2026 16:32:34 +0200 Subject: [PATCH 08/16] Updated README.md --- README.md | 4 +++- actions/show_icon/icon_window.py | 2 +- assets/show_icon.png | Bin 20576 -> 28690 bytes assets/show_icon_customize.png | Bin 21491 -> 30315 bytes 4 files changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 6ab3819..13c4b45 100644 --- a/README.md +++ b/README.md @@ -58,11 +58,13 @@ - Disable `key_down` by mapping it to `None` ### 🎨 Show an Icon -- Display entity icons or custom icons from [Material Design Icons](https://pictogrammers.com/library/mdi/) +- Display entity icons or custom icons from [Material Design Icons](https://pictogrammers.com/library/mdi/) as well as images and GIFs - Customize icon appearance: - 🎨 Color - 📏 Scale - 🌫️ Opacity +- Customize image appearance: + - 📏 Scale - **Dynamic customization** based on state or attribute values - Automatic updates when entity state changes diff --git a/actions/show_icon/icon_window.py b/actions/show_icon/icon_window.py index a86c90b..31f81f4 100644 --- a/actions/show_icon/icon_window.py +++ b/actions/show_icon/icon_window.py @@ -54,7 +54,7 @@ def __init__(self, lm, attributes: list, callback: Callable, browse_button.set_margin_top(self.default_margin) browse_button.set_margin_bottom(self.default_margin) browse_button.set_margin_start(self.default_margin) - browse_button.set_margin_end(self.default_margin) + browse_button.set_margin_end(15) self.connect_rows.append(partial(browse_button.connect, base_const.CONNECT_CLICKED, self._on_browse_clicked)) self.color = self._create_color_button(self.check_color) diff --git a/assets/show_icon.png b/assets/show_icon.png index d858b9f69bc4727e450619c072ebf8a7ff0f4ea4..56856ca04a96bc40f583ff37f72fc0f8929c7d62 100644 GIT binary patch literal 28690 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 diff --git a/assets/show_icon_customize.png b/assets/show_icon_customize.png index 3c637601503a094d3a951a044e5aefebc111a555..eda68dd225dcd3c15ba176edf8e36f73b1c89c58 100644 GIT binary patch literal 30315 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 T^cp_Rm2yN?Q{~Bl)1LnYpLul^ literal 21491 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 From daf19346d41ae948e79e6e1abcb5d4a181ef0e89 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 18 May 2026 14:53:45 +0000 Subject: [PATCH 09/16] test: update show_icon tests for browse button and icon gating Agent-Logs-Url: https://github.com/gensyn/HomeAssistantPlugin/sessions/6527bd55-e8ca-42ab-8a59-63115fd1298c Co-authored-by: gensyn <36128035+gensyn@users.noreply.github.com> --- test/actions/show_icon/test_icon_action.py | 10 ++++++++-- test/actions/show_icon/test_icon_window.py | 5 +++-- 2 files changed, 11 insertions(+), 4 deletions(-) diff --git a/test/actions/show_icon/test_icon_action.py b/test/actions/show_icon/test_icon_action.py index cb8ff5a..176c760 100644 --- a/test/actions/show_icon/test_icon_action.py +++ b/test/actions/show_icon/test_icon_action.py @@ -10,6 +10,7 @@ sys.path.insert(0, absolute_plugin_path) from HomeAssistantPlugin.actions.show_icon import icon_const +from HomeAssistantPlugin.actions.show_icon import icon_helper from HomeAssistantPlugin.actions.show_icon.icon_action import ShowIcon from HomeAssistantPlugin.actions.show_icon.icon_customization import IconCustomization from HomeAssistantPlugin.actions.show_icon.icon_row import IconRow @@ -68,9 +69,12 @@ def test_get_config_rows(self): @patch('HomeAssistantPlugin.actions.show_icon.icon_action.EntryRow') @patch('HomeAssistantPlugin.actions.show_icon.icon_action.ColorButtonRow') @patch('HomeAssistantPlugin.actions.show_icon.icon_action.ScaleRow') - def testcreate_ui_elements(self, scale_row_mock, color_button_row_mock, entry_row_mock, super_create_ui_elements_mock): + @patch('HomeAssistantPlugin.actions.show_icon.icon_action.Button') + def testcreate_ui_elements(self, button_mock, scale_row_mock, color_button_row_mock, entry_row_mock, super_create_ui_elements_mock): instance = ShowIcon.__new__(ShowIcon) instance._reload = Mock() + instance.lm = Mock() + instance.lm.get.return_value = "Browse" instance.create_ui_elements() @@ -162,10 +166,12 @@ def test_set_enabled_disabled_success(self, super_set_enabled_disabled_mock): instance.settings = Mock() instance.settings.get_domain.return_value = "domain" instance.settings.get_entity.return_value = "entity" + instance.settings.get_icon.return_value = next(iter(icon_helper.MDI_ICONS)) instance.icon = Mock() instance.color = Mock() instance.scale = Mock() instance.opacity = Mock() + instance.lm = Mock() instance.set_enabled_disabled() @@ -273,4 +279,4 @@ def test_get_domains(self): result = instance._get_domains() instance.plugin_base.backend.get_domains_for_entities.assert_called_once() - self.assertEqual(result, ["domain1", "domain2"]) \ No newline at end of file + self.assertEqual(result, ["domain1", "domain2"]) diff --git a/test/actions/show_icon/test_icon_window.py b/test/actions/show_icon/test_icon_window.py index 4c6180c..6a5fd56 100644 --- a/test/actions/show_icon/test_icon_window.py +++ b/test/actions/show_icon/test_icon_window.py @@ -24,8 +24,9 @@ class TestIconWindow(unittest.TestCase): @patch.object(IconWindow, "_create_scale") @patch.object(IconWindow, "_create_scale_entry") @patch.object(IconWindow, "_create_label") + @patch('HomeAssistantPlugin.actions.show_icon.icon_window.Button') def test_init(self, create_label_mock, create_scale_entry_mock, create_scale_mock, create_color_button_mock, - create_entry_mock, create_check_button_mock, after_init_mock, set_title_mock, + create_entry_mock, create_check_button_mock, after_init_mock, set_title_mock, button_mock, customization_window_init_mock): lm = Mock() lm.get.return_value = "Test Title" @@ -51,7 +52,7 @@ def super_init(instance, lm, *args, **kwargs): customization_window_init_mock.assert_called_once_with(instance, lm, attributes, callback, current, index) # with instance because of autospec set_title_mock.assert_called_once_with("Test Title") - self.assertEqual(14, instance.grid_fields.attach.call_count) + self.assertEqual(15, instance.grid_fields.attach.call_count) after_init_mock.assert_called_once() @patch( From 3db6824912d73443e166a30cdc5d2f444f54a09a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 18 May 2026 14:56:22 +0000 Subject: [PATCH 10/16] test: align on_ready and icon window tests with merged behavior Agent-Logs-Url: https://github.com/gensyn/HomeAssistantPlugin/sessions/6527bd55-e8ca-42ab-8a59-63115fd1298c Co-authored-by: gensyn <36128035+gensyn@users.noreply.github.com> --- .../cores/base_core/test_base_core_on_ready.py | 13 +++++++++---- test/actions/show_icon/test_icon_window.py | 7 +++---- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/test/actions/cores/base_core/test_base_core_on_ready.py b/test/actions/cores/base_core/test_base_core_on_ready.py index 1dc9076..0c06631 100644 --- a/test/actions/cores/base_core/test_base_core_on_ready.py +++ b/test/actions/cores/base_core/test_base_core_on_ready.py @@ -18,7 +18,8 @@ class TestBaseCoreOnReady(unittest.TestCase): @patch.object(BaseCore, "_create_event_assigner") @patch.object(BaseCore, "_load_domains") @patch.object(BaseCore, "_load_entities") - def test_on_read_no_entity(self, load_entities_mock, load_domains_mock, _, __): + @patch('HomeAssistantPlugin.actions.cores.base_core.base_core.migrate_settings') + def test_on_read_no_entity(self, migrate_settings_mock, load_entities_mock, load_domains_mock, _, __): track_entity = True settings_implementation = Mock() @@ -28,6 +29,7 @@ def test_on_read_no_entity(self, load_entities_mock, load_domains_mock, _, __): instance = BaseCore(settings_implementation, track_entity) instance.on_ready() + migrate_settings_mock.assert_called_once_with(instance) instance.plugin_base.backend.add_action_ready_callback.assert_called_once_with(instance.on_ready) settings_implementation.get_entity.assert_called_once() instance.plugin_base.backend.add_tracked_entity.assert_not_called() @@ -38,7 +40,8 @@ def test_on_read_no_entity(self, load_entities_mock, load_domains_mock, _, __): @patch.object(BaseCore, "_create_event_assigner") @patch.object(BaseCore, "_load_domains") @patch.object(BaseCore, "_load_entities") - def test_on_read_entity_not_tracked(self, load_entities_mock, load_domains_mock, _, __): + @patch('HomeAssistantPlugin.actions.cores.base_core.base_core.migrate_settings') + def test_on_read_entity_not_tracked(self, migrate_settings_mock, load_entities_mock, load_domains_mock, _, __): track_entity = False settings_implementation = Mock() @@ -48,6 +51,7 @@ def test_on_read_entity_not_tracked(self, load_entities_mock, load_domains_mock, instance = BaseCore(settings_implementation, track_entity) instance.on_ready() + migrate_settings_mock.assert_called_once_with(instance) instance.plugin_base.backend.add_action_ready_callback.assert_called_once_with(instance.on_ready) settings_implementation.get_entity.assert_called_once() instance.plugin_base.backend.add_tracked_entity.assert_not_called() @@ -58,7 +62,8 @@ def test_on_read_entity_not_tracked(self, load_entities_mock, load_domains_mock, @patch.object(BaseCore, "_create_event_assigner") @patch.object(BaseCore, "_load_domains") @patch.object(BaseCore, "_load_entities") - def test_on_read_success(self, load_entities_mock, load_domains_mock, _, __): + @patch('HomeAssistantPlugin.actions.cores.base_core.base_core.migrate_settings') + def test_on_read_success(self, migrate_settings_mock, load_entities_mock, load_domains_mock, _, __): track_entity = True settings_implementation = Mock() @@ -68,9 +73,9 @@ def test_on_read_success(self, load_entities_mock, load_domains_mock, _, __): instance = BaseCore(settings_implementation, track_entity) instance.on_ready() + migrate_settings_mock.assert_called_once_with(instance) instance.plugin_base.backend.add_action_ready_callback.assert_called_once_with(instance.on_ready) settings_implementation.get_entity.assert_called_once() instance.plugin_base.backend.add_tracked_entity.assert_called_once_with("entity", instance.refresh) load_entities_mock.assert_called_once() load_domains_mock.assert_called_once() - diff --git a/test/actions/show_icon/test_icon_window.py b/test/actions/show_icon/test_icon_window.py index 6a5fd56..4fe8a6b 100644 --- a/test/actions/show_icon/test_icon_window.py +++ b/test/actions/show_icon/test_icon_window.py @@ -25,8 +25,8 @@ class TestIconWindow(unittest.TestCase): @patch.object(IconWindow, "_create_scale_entry") @patch.object(IconWindow, "_create_label") @patch('HomeAssistantPlugin.actions.show_icon.icon_window.Button') - def test_init(self, create_label_mock, create_scale_entry_mock, create_scale_mock, create_color_button_mock, - create_entry_mock, create_check_button_mock, after_init_mock, set_title_mock, button_mock, + def test_init(self, button_mock, create_label_mock, create_scale_entry_mock, create_scale_mock, create_color_button_mock, + create_entry_mock, create_check_button_mock, after_init_mock, set_title_mock, customization_window_init_mock): lm = Mock() lm.get.return_value = "Test Title" @@ -140,8 +140,7 @@ def test_on_add_button_icon_not_valid(self, super_on_add_button_mock): instance.check_icon = Mock() instance.check_icon.get_active.return_value = True instance.icon = Mock() - instance.icon.get_text.return_value = "mdi:home" - instance.icons = ["not_home"] + instance.icon.get_text.return_value = "" instance.on_add_button() super_on_add_button_mock.assert_called_once() From e3cd90d431c2fae28c5f2e102da186f96daa88cb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 18 May 2026 14:56:57 +0000 Subject: [PATCH 11/16] test: clarify icon window validation test name Agent-Logs-Url: https://github.com/gensyn/HomeAssistantPlugin/sessions/6527bd55-e8ca-42ab-8a59-63115fd1298c Co-authored-by: gensyn <36128035+gensyn@users.noreply.github.com> --- test/actions/show_icon/test_icon_window.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/actions/show_icon/test_icon_window.py b/test/actions/show_icon/test_icon_window.py index 4fe8a6b..a716bce 100644 --- a/test/actions/show_icon/test_icon_window.py +++ b/test/actions/show_icon/test_icon_window.py @@ -133,7 +133,7 @@ def test_on_add_button_super_false(self, super_on_add_button_mock): @patch( 'HomeAssistantPlugin.actions.cores.customization_core.customization_window.CustomizationWindow.on_add_button') - def test_on_add_button_icon_not_valid(self, super_on_add_button_mock): + def test_on_add_button_icon_checked_but_empty(self, super_on_add_button_mock): super_on_add_button_mock.return_value = True instance = IconWindow.__new__(IconWindow) From 1b503a2ae397931bc1a1de2e16f47fc9161c6a78 Mon Sep 17 00:00:00 2001 From: gensyn <36128035+gensyn@users.noreply.github.com> Date: Fri, 22 May 2026 14:03:30 +0200 Subject: [PATCH 12/16] Create FUNDING.yml --- .github/FUNDING.yml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 .github/FUNDING.yml diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 0000000..a7b7452 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,15 @@ +# These are supported funding model platforms + +github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] +patreon: # Replace with a single Patreon username +open_collective: # Replace with a single Open Collective username +ko_fi: gensyn +tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel +community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry +liberapay: # Replace with a single Liberapay username +issuehunt: # Replace with a single IssueHunt username +lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry +polar: # Replace with a single Polar username +buy_me_a_coffee: # Replace with a single Buy Me a Coffee username +thanks_dev: # Replace with a single thanks.dev username +custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] From c9d0681e3e516e89caeb48731a472ce902dadc9f Mon Sep 17 00:00:00 2001 From: gensyn <36128035+gensyn@users.noreply.github.com> Date: Fri, 22 May 2026 14:22:23 +0200 Subject: [PATCH 13/16] Remove support section and GitHub badge from README Removed GitHub badge and support section from README. --- README.md | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 13c4b45..4666662 100644 --- a/README.md +++ b/README.md @@ -398,12 +398,11 @@ Contributions are welcome! Feel free to: - 📝 Improve documentation - 🔧 Submit pull requests ---- -
+--- -**Made with ❤️ for the Home Assistant and StreamController communities** +## ⭐ Support -[![GitHub](https://img.shields.io/badge/GitHub-Repository-181717?logo=github)](https://github.com/gensyn/HomeAssistantPlugin) +If you find HomeAssistantPlugin useful, please consider giving it a star on GitHub! It helps others discover the project. -
+[![ko-fi](https://ko-fi.com/img/githubbutton_sm.svg)](https://ko-fi.com/M5G61ZZY8D) From 0e08625e50a2b1c02515f1553b46c1e476845c52 Mon Sep 17 00:00:00 2001 From: gensyn Date: Fri, 3 Jul 2026 15:58:30 +0200 Subject: [PATCH 14/16] Cleaned up code for image support --- actions/cores/base_core/base_core.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/actions/cores/base_core/base_core.py b/actions/cores/base_core/base_core.py index 5c6a421..2b42d10 100644 --- a/actions/cores/base_core/base_core.py +++ b/actions/cores/base_core/base_core.py @@ -117,7 +117,11 @@ def on_change_domain(self, _, domain, old_domain): if entity and self.track_entity: self.plugin_base.backend.remove_tracked_entity(entity, self.refresh) self.settings.reset(domain) + # save entities from the combo to a temporary variable to keep them alive while we clear the combo + _temp_keep_alive = [self.entity_combo.get_item(i) for i in range(self.entity_combo.get_n_items())] self.entity_combo.remove_all_items() + # now the entities can be removed + del _temp_keep_alive self._last_loaded_entities = None if domain: From 4a03dfd1b3b701e9d23de68cbdecce7ee5042f56 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 3 Jul 2026 14:06:47 +0000 Subject: [PATCH 15/16] Make keep-alive logic defensive with try/except for TypeError --- actions/cores/base_core/base_core.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/actions/cores/base_core/base_core.py b/actions/cores/base_core/base_core.py index 2b42d10..41e2887 100644 --- a/actions/cores/base_core/base_core.py +++ b/actions/cores/base_core/base_core.py @@ -118,7 +118,10 @@ def on_change_domain(self, _, domain, old_domain): self.plugin_base.backend.remove_tracked_entity(entity, self.refresh) self.settings.reset(domain) # save entities from the combo to a temporary variable to keep them alive while we clear the combo - _temp_keep_alive = [self.entity_combo.get_item(i) for i in range(self.entity_combo.get_n_items())] + try: + _temp_keep_alive = [self.entity_combo.get_item(i) for i in range(self.entity_combo.get_n_items())] + except TypeError: + _temp_keep_alive = [] self.entity_combo.remove_all_items() # now the entities can be removed del _temp_keep_alive From 38c0836fdb9e5f88918194d794362ac062540a9a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 3 Jul 2026 14:08:54 +0000 Subject: [PATCH 16/16] Fix tests: configure entity_combo mock with get_n_items returning 0 --- actions/cores/base_core/base_core.py | 5 +---- .../cores/base_core/test_base_core_on_change_domain.py | 5 +++++ 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/actions/cores/base_core/base_core.py b/actions/cores/base_core/base_core.py index 41e2887..2b42d10 100644 --- a/actions/cores/base_core/base_core.py +++ b/actions/cores/base_core/base_core.py @@ -118,10 +118,7 @@ def on_change_domain(self, _, domain, old_domain): self.plugin_base.backend.remove_tracked_entity(entity, self.refresh) self.settings.reset(domain) # save entities from the combo to a temporary variable to keep them alive while we clear the combo - try: - _temp_keep_alive = [self.entity_combo.get_item(i) for i in range(self.entity_combo.get_n_items())] - except TypeError: - _temp_keep_alive = [] + _temp_keep_alive = [self.entity_combo.get_item(i) for i in range(self.entity_combo.get_n_items())] self.entity_combo.remove_all_items() # now the entities can be removed del _temp_keep_alive diff --git a/test/actions/cores/base_core/test_base_core_on_change_domain.py b/test/actions/cores/base_core/test_base_core_on_change_domain.py index 8e34a95..faff3a6 100644 --- a/test/actions/cores/base_core/test_base_core_on_change_domain.py +++ b/test/actions/cores/base_core/test_base_core_on_change_domain.py @@ -58,6 +58,7 @@ def test_on_change_domain_no_entity(self, set_enabled_disabled_mock, load_entiti entity_combo_mock = Mock() entity_combo_mock.remove_all_items = remove_all_items_mock + entity_combo_mock.get_n_items = Mock(return_value=0) domain = "light" @@ -89,6 +90,7 @@ def test_on_change_domain_enity_not_tracked(self, set_enabled_disabled_mock, loa entity_combo_mock = Mock() entity_combo_mock.remove_all_items = remove_all_items_mock + entity_combo_mock.get_n_items = Mock(return_value=0) domain = "light" @@ -120,6 +122,7 @@ def test_on_change_domain_no_new_domain(self, set_enabled_disabled_mock, load_en entity_combo_mock = Mock() entity_combo_mock.remove_all_items = remove_all_items_mock + entity_combo_mock.get_n_items = Mock(return_value=0) domain = None @@ -151,6 +154,7 @@ def test_on_change_domain_success(self, set_enabled_disabled_mock, load_entities entity_combo_mock = Mock() entity_combo_mock.remove_all_items = remove_all_items_mock + entity_combo_mock.get_n_items = Mock(return_value=0) domain = "light" @@ -180,6 +184,7 @@ def test_on_change_domain_resets_entity_cache(self, set_enabled_disabled_mock, l settings_mock.reset = Mock() entity_combo_mock = Mock() + entity_combo_mock.get_n_items = Mock(return_value=0) instance = BaseCore(Mock(), True) instance.initialized = True