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'] diff --git a/README.md b/README.md index 6ab3819..4666662 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 @@ -396,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) 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 2f6c41e..2b42d10 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_settings + +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_settings(self) self.settings = self.settings_implementation(self) self.initialized = True @@ -114,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: 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 new file mode 100644 index 0000000..c122dfe --- /dev/null +++ b/actions/cores/base_core/migrate.py @@ -0,0 +1,27 @@ +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_settings(action) -> None: + settings = action.get_settings() + + if settings.get(const.SETTING_VERSION) is not None: + return + + migrate_v0_v1(action) + +def migrate_v0_v1(action) -> None: + """ + Migrate from no version to v1. + There is actually nothing to do yet - this just introduces the version field. + """ + settings = action.get_settings() + + if not settings: + # settings are empty - this is a brand-new action + return + + 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 3a2fdb6..c715d83 100644 --- a/actions/show_icon/icon_action.py +++ b/actions/show_icon/icon_action.py @@ -2,6 +2,16 @@ 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 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 @@ -43,6 +53,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, @@ -63,6 +78,7 @@ def create_ui_elements(self) -> None: can_reset=False, complex_var_name=True ) + @requires_initialization def set_enabled_disabled(self) -> None: """ @@ -87,17 +103,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)) + else: + icon_value = self.settings.get_icon() + 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(True) - self.color.widget.set_subtitle(icon_const.EMPTY_STRING) + 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(True) - self.opacity.widget.set_subtitle(icon_const.EMPTY_STRING) + self.opacity.widget.set_sensitive(has_icon) + self.opacity.widget.set_subtitle(not_supported) def refresh(self, state: dict = None) -> None: """ @@ -123,6 +144,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.icon.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.icon.widget.set_text(file.get_path()) + self._reload() + 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.""" 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 fef9395..cdbd391 100644 --- a/actions/show_icon/icon_const.py +++ b/actions/show_icon/icon_const.py @@ -31,6 +31,11 @@ CUSTOM_COLOR = "color" CUSTOM_SCALE = "scale" CUSTOM_OPACITY = "opacity" +CUSTOM_IMAGE = "image" + +LABEL_ICON_IMAGE = "actions.home_assistant.icon.image.label" +LABEL_ICON_BROWSE = "actions.home_assistant.icon.browse.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" @@ -39,4 +44,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 93ec74f..606b0c4 100644 --- a/actions/show_icon/icon_customization.py +++ b/actions/show_icon/icon_customization.py @@ -73,4 +73,4 @@ def export(self) -> dict[str, Any]: icon_const.CUSTOM_COLOR: self.color, icon_const.CUSTOM_SCALE: self.scale, 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 d052add..8d39c1c 100644 --- a/actions/show_icon/icon_helper.py +++ b/actions/show_icon/icon_helper.py @@ -29,6 +29,10 @@ def get_icon(state: dict, settings: ShowIconSettings, is_connected: bool) -> tup name, color, scale, opacity = _get_icon_settings(state, settings) + 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) @@ -39,14 +43,11 @@ 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) + 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() - if settings.get_icon() in MDI_ICONS.keys(): - name = settings.get_icon() - # # Begin custom icon # @@ -88,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 = _replace_values(name, color, scale, opacity, - customization) + name, color, scale, opacity = _replace_values(name, color, scale, opacity, customization) # # End custom icon @@ -154,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_settings.py b/actions/show_icon/icon_settings.py index 68304d1..523f5c0 100644 --- a/actions/show_icon/icon_settings.py +++ b/actions/show_icon/icon_settings.py @@ -54,4 +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 c4b8215..31f81f4 100644 --- a/actions/show_icon/icon_window.py +++ b/actions/show_icon/icon_window.py @@ -2,9 +2,17 @@ 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 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 @@ -39,6 +47,15 @@ def __init__(self, lm, attributes: list, callback: Callable, 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.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) + browse_button.set_margin_bottom(self.default_margin) + browse_button.set_margin_start(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) @@ -75,6 +92,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) @@ -112,6 +130,7 @@ def set_current_values(self) -> 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(): rgba = customization_helper.convert_color_list_to_rgba(self.current.get_color()) @@ -134,18 +153,12 @@ 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_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()): + 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) @@ -156,8 +169,7 @@ def on_add_button(self, *_, **__) -> 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 + 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(), @@ -167,6 +179,43 @@ def on_add_button(self, *_, **__) -> None: self.destroy() + def _on_icon_changed(self, *_) -> None: + icon_value = self.icon.get_text() + 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(has_icon) + + 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.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)) + + 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.icon.set_text(file.get_path()) + self.check_icon.set_active(True) + self._on_icon_changed() + except Exception as e: + log.error(f"Error choosing file: {e}") + def on_widget_changed(self, *_, **__) -> None: super().on_widget_changed() diff --git a/assets/show_icon.png b/assets/show_icon.png index d858b9f..56856ca 100644 Binary files a/assets/show_icon.png and b/assets/show_icon.png differ diff --git a/assets/show_icon_customize.png b/assets/show_icon_customize.png index 3c63760..eda68dd 100644 Binary files a/assets/show_icon_customize.png and b/assets/show_icon_customize.png differ diff --git a/locales/de_DE.json b/locales/de_DE.json index b889e60..47ebac7 100644 --- a/locales/de_DE.json +++ b/locales/de_DE.json @@ -28,6 +28,9 @@ "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.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 d921814..1ad2f41 100644 --- a/locales/en_US.json +++ b/locales/en_US.json @@ -28,6 +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.browse.label": "Browse...", + "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:", 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 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_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..a716bce 100644 --- a/test/actions/show_icon/test_icon_window.py +++ b/test/actions/show_icon/test_icon_window.py @@ -24,7 +24,8 @@ class TestIconWindow(unittest.TestCase): @patch.object(IconWindow, "_create_scale") @patch.object(IconWindow, "_create_scale_entry") @patch.object(IconWindow, "_create_label") - def test_init(self, create_label_mock, create_scale_entry_mock, create_scale_mock, create_color_button_mock, + @patch('HomeAssistantPlugin.actions.show_icon.icon_window.Button') + 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() @@ -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( @@ -132,15 +133,14 @@ 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) 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()