Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/pylint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ jobs:

strategy:
matrix:
python-version: ["3.12", "3.13"]
python-version: ["3.13"]
Copy link

Copilot AI Mar 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The pylint workflow also removes Python 3.12 from the matrix. If 3.12 remains a supported runtime, consider keeping it here as well (or documenting the intentional support drop in the PR description).

Suggested change
python-version: ["3.13"]
python-version: ["3.12", "3.13"]

Copilot uses AI. Check for mistakes.

steps:
- uses: actions/checkout@v4
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ jobs:

strategy:
matrix:
python-version: ["3.12", "3.13"]
python-version: ["3.13"]
Copy link

Copilot AI Mar 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The CI workflow drops Python 3.12 from the test matrix. If the project still intends to support 3.12 (as it did previously), this reduces coverage and can let compatibility regressions slip in. If 3.12 support is intentionally being removed, it would be good to note that explicitly in the PR description (and/or any support policy docs).

Suggested change
python-version: ["3.13"]
python-version: ["3.12", "3.13"]

Copilot uses AI. Check for mistakes.

steps:
- uses: actions/checkout@v4
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -380,7 +380,7 @@ Currently, all planned features have been implemented! Have a suggestion? [Open

If you encounter any problems or have questions:

1. 📖 Check the [documentation](#documentation) above
1. 📖 Check the [documentation](#-documentation) above
2. 🔍 Search [existing issues](https://github.com/gensyn/HomeAssistantPlugin/issues)
3. 🐛 [Open a new issue](https://github.com/gensyn/HomeAssistantPlugin/issues/new) with:
- Detailed description of the problem
Expand Down
49 changes: 33 additions & 16 deletions actions/cores/base_core/base_core.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,30 @@
"""The module for the Home Assistant action that is loaded in StreamController."""

from typing import List

import gi
from GtkHelper.GenerativeUI.ComboRow import ComboRow
from HomeAssistantPlugin.actions import const
from src.backend.PluginManager.ActionCore import ActionCore

gi.require_version('Gtk', '4.0')
from gi.repository import Gtk


def set_substring_search(combo_row: ComboRow) -> None:
"""Enable substring search mode on a ComboRow if supported by the installed libadwaita version."""
try:
widget = combo_row.widget
if hasattr(widget, "set_search_match_mode"):
widget.set_search_match_mode(Gtk.StringFilterMatchMode.SUBSTRING)
except AttributeError:
pass
Comment on lines 4 to +19
Copy link

Copilot AI Mar 8, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

base_core.py imports Gtk directly at module import time and without gi.require_version('Gtk', '4.0'). This differs from the pattern used elsewhere in the repo and also contradicts the PR description about a module-level try/except for GTK to keep tests/headless imports working. Consider importing gi + calling gi.require_version before importing Gtk, and guarding the Gtk import (or moving it inside _set_substring_search) so the module can still be imported when GTK isn’t available; _set_substring_search can then no-op when Gtk is missing.

Copilot uses AI. Check for mistakes.


def requires_initialization(func):
def wrapper(self, *args, **kwargs):
if not getattr(self, 'initialized', False):
return None
return func(self, *args, **kwargs)

return wrapper


Expand All @@ -26,7 +39,9 @@ def __init__(self, settings_implementation, track_entity: bool, *args, **kwargs)
self.lm = self.plugin_base.locale_manager
self.has_configuration = True
self.track_entity = track_entity
self._create_ui_elements()
self.domain_combo = None
self.entity_combo = None
self.create_ui_elements()
self._create_event_assigner()

def on_ready(self) -> None:
Expand Down Expand Up @@ -55,34 +70,36 @@ def on_remove(self) -> None:
)
self.refresh()

def get_config_rows(self) -> List:
def get_config_rows(self) -> list:
"""Get the rows to be displayed in the UI."""
raise NotImplementedError("Must be implemented by subclasses.")

def _create_ui_elements(self) -> None:
def create_ui_elements(self) -> None:
"""Get all entity rows."""
self.domain_combo: ComboRow = ComboRow(
self, const.SETTING_ENTITY_DOMAIN, const.EMPTY_STRING, [],
const.LABEL_ENTITY_DOMAIN, enable_search=True,
on_change=self._on_change_domain, can_reset=False,
on_change=self.on_change_domain, can_reset=False,
complex_var_name=True
)
set_substring_search(self.domain_combo)

self.entity_combo: ComboRow = ComboRow(
self, const.SETTING_ENTITY_ENTITY, const.EMPTY_STRING, [],
const.LABEL_ENTITY_ENTITY, enable_search=True,
on_change=self._on_change_entity, can_reset=False,
on_change=self.on_change_entity, can_reset=False,
complex_var_name=True
)
set_substring_search(self.entity_combo)

@requires_initialization
def _reload(self, *_):
"""Reload the action."""
self._set_enabled_disabled()
self.set_enabled_disabled()
self.refresh()

@requires_initialization
def _on_change_domain(self, _, domain, old_domain):
def on_change_domain(self, _, domain, old_domain):
"""Execute when the domain is changed."""
domain = str(domain) if domain is not None else None
old_domain = str(old_domain) if old_domain is not None else None
Expand All @@ -97,10 +114,10 @@ def _on_change_domain(self, _, domain, old_domain):
if domain:
self._load_entities()

self._set_enabled_disabled()
self.set_enabled_disabled()

@requires_initialization
def _on_change_entity(self, _, entity, old_entity):
def on_change_entity(self, _, entity, old_entity):
"""Execute when the entity is changed."""
entity = str(entity) if entity is not None else None
old_entity = str(old_entity) if old_entity is not None else None
Expand All @@ -112,7 +129,7 @@ def _on_change_entity(self, _, entity, old_entity):
self.plugin_base.backend.add_tracked_entity(entity, self.refresh)

self.refresh()
self._set_enabled_disabled()
self.set_enabled_disabled()

@requires_initialization
def refresh(self, state: dict = None) -> None:
Expand Down Expand Up @@ -156,28 +173,28 @@ def _load_entities(self) -> None:
if entities != self._get_current_entities():
self.entity_combo.populate(entities, entity, trigger_callback=False)

def _get_current_domains(self) -> List[str]:
def _get_current_domains(self) -> list[str]:
"""Get the domains currently displayed in the domain combo."""
return [
str(self.domain_combo.get_item_at(i))
for i in range(self.domain_combo.get_item_amount())
]

def _get_current_entities(self) -> List[str]:
def _get_current_entities(self) -> list[str]:
"""Get the entities currently displayed in the entity combo."""
return [
str(self.entity_combo.get_item_at(i))
for i in range(self.entity_combo.get_item_amount())
]

@requires_initialization
def _set_enabled_disabled(self) -> None:
def set_enabled_disabled(self) -> None:
"""Set the active/inactive state for all rows."""
domain = self.settings.get_domain()
is_domain_set = bool(domain)
self.entity_combo.set_sensitive(is_domain_set)

def _get_domains(self) -> List[str]:
def _get_domains(self) -> list[str]:
"""Get the domains available in Home Assistant."""
raise NotImplementedError("Must be implemented by subclasses.")

Expand Down
5 changes: 3 additions & 2 deletions actions/cores/customization_core/customization.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@
Modul to manage customizations.
"""

from typing import Dict, Any
from typing import Any


class Customization:
"""
Base class to represent a customization.
"""

def __init__(self, attribute: str, operator: str, value: str):
self.attribute: str = attribute
self.operator: str = operator
Expand All @@ -35,7 +36,7 @@ def get_value(self) -> str:
"""
return self.value

def export(self) -> Dict[str, Any]:
def export(self) -> dict[str, Any]:
"""
Get this customization as a dict. Must be implemented in a subclass.
:return: this customization as a dict
Expand Down
14 changes: 7 additions & 7 deletions actions/cores/customization_core/customization_core.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
"""The module for the Home Assistant action that is loaded in StreamController."""
from copy import deepcopy
from typing import List

import gi


gi.require_version("Gtk", "4.0")
from gi.repository.Gtk import Button, Align

Expand All @@ -19,6 +17,8 @@ class CustomizationCore(BaseCore):
"""Action core for all Home Assistant Actions."""

def __init__(self, window_implementation, customization_implementation, row_implementation, *args, **kwargs):
# Must be set before create_ui_elements in BaseCore is called
self.customization_expander = None
super().__init__(*args, **kwargs)
self.window_implementation = window_implementation
self.customization_implementation = customization_implementation
Expand All @@ -34,9 +34,9 @@ def on_ready(self) -> None:

self._reload()

def _create_ui_elements(self) -> None:
def create_ui_elements(self) -> None:
"""Get all action rows."""
super()._create_ui_elements()
super().create_ui_elements()

add_customization_button = Button(icon_name="list-add", valign=Align.CENTER)
add_customization_button.set_size_request(15, 15)
Expand Down Expand Up @@ -99,11 +99,11 @@ def _on_move_down(self, _, index: int):
self.refresh()

@requires_initialization
def _set_enabled_disabled(self) -> None:
def set_enabled_disabled(self) -> None:
"""
Set the active/inactive state for all rows.
"""
super()._set_enabled_disabled()
super().set_enabled_disabled()

domain = self.settings.get_domain()
is_domain_set = bool(domain)
Expand All @@ -122,7 +122,7 @@ def _set_enabled_disabled(self) -> None:
len(self.settings.get_customizations()) > 0
)

def _get_attributes(self) -> List[str]:
def _get_attributes(self) -> list[str]:
"""
Gets the list of attributes for the selected entity.
:return: the list of attributes
Expand Down
12 changes: 5 additions & 7 deletions actions/cores/customization_core/customization_helper.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
"""
Module for helper functions.
"""
from typing import Tuple

import gi

gi.require_version("Gdk", "4.0")
from gi.repository.Gdk import RGBA



def convert_color_list_to_rgba(color: Tuple[int, int, int, int]) -> RGBA:
def convert_color_list_to_rgba(color: tuple[int, int, int, int]) -> RGBA:
"""
Converts a list with RGB values to an RGBA object.
The alpha value is always set to 1.
Expand All @@ -24,17 +22,17 @@ def convert_color_list_to_rgba(color: Tuple[int, int, int, int]) -> RGBA:
return rgba


def convert_rgba_to_color_list(rgba: RGBA) -> Tuple[int, int, int, int]:
def convert_rgba_to_color_list(rgba: RGBA) -> tuple[int, int, int, int]:
"""
Converts an RGBA object to a list with RGB values.
The alpha value is always set to 255.
:param rgba: the RGBA value to convert
:return: the color list
"""
return int(rgba.red*255), int(rgba.green*255), int(rgba.blue*255), 255
return int(rgba.red * 255), int(rgba.green * 255), int(rgba.blue * 255), 255


def convert_color_list_to_hex(color: Tuple[int, int, int, int]) -> str:
def convert_color_list_to_hex(color: tuple[int, int, int, int]) -> str:
"""
Converts a tuple with RGBA values to a hex string representation.
:param color: the color to convert
Expand Down
5 changes: 2 additions & 3 deletions actions/cores/customization_core/customization_row.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
"""
The module for the Home Assistant customization row.
"""
from typing import List, Dict, Any
from typing import Any

import gi

from HomeAssistantPlugin.actions.cores.customization_core import customization_const
from HomeAssistantPlugin.actions.cores.customization_core.customization import Customization
from HomeAssistantPlugin.actions.cores.customization_core.customization_settings import CustomizationSettings
Expand All @@ -20,7 +19,7 @@ class CustomizationRow(ActionRow):
Base for customization rows
"""

def __init__(self, lm, customization_count: int, index: int, attributes: List, state: Dict,
def __init__(self, lm, customization_count: int, index: int, attributes: list, state: dict,
settings: CustomizationSettings):
super().__init__()

Expand Down
10 changes: 5 additions & 5 deletions actions/cores/customization_core/customization_settings.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,5 @@
"""Module to manage HomeAssistantPlugin action settings."""

from typing import List

from HomeAssistantPlugin.actions.cores.base_core.base_settings import BaseSettings
from HomeAssistantPlugin.actions.cores.customization_core import customization_const
from HomeAssistantPlugin.actions.cores.customization_core.customization import Customization
Expand All @@ -20,7 +18,8 @@ def __init__(self, action, customization_name, customization_implementation):
self.customization_implementation = customization_implementation

def get_customizations(self):
return [self.customization_implementation.from_dict(c) for c in self._action.get_settings()[self.customization_name][customization_const.SETTING_CUSTOMIZATIONS]]
return [self.customization_implementation.from_dict(c) for c in
self._action.get_settings()[self.customization_name][customization_const.SETTING_CUSTOMIZATIONS]]

def move_customization(self, index: int, offset: int):
"""
Expand All @@ -31,7 +30,8 @@ def move_customization(self, index: int, offset: int):
"""
settings = self._action.get_settings()
customization = settings[self.customization_name][customization_const.SETTING_CUSTOMIZATIONS].pop(index)
settings[self.customization_name][customization_const.SETTING_CUSTOMIZATIONS].insert(index + offset, customization)
settings[self.customization_name][customization_const.SETTING_CUSTOMIZATIONS].insert(index + offset,
customization)
self._action.set_settings(settings)

def remove_customization(self, index: int) -> None:
Expand Down Expand Up @@ -62,4 +62,4 @@ def add_customization(self, customization: Customization) -> None:
"""
settings = self._action.get_settings()
settings[self.customization_name][customization_const.SETTING_CUSTOMIZATIONS].append(customization.export())
self._action.set_settings(settings)
self._action.set_settings(settings)
Loading