Skip to content

Commit

Permalink
Home Assistant 2023.3 Support
Browse files Browse the repository at this point in the history
  • Loading branch information
vincentscode committed Apr 14, 2023
1 parent 1f87ae8 commit b9ae808
Show file tree
Hide file tree
Showing 36 changed files with 512 additions and 232 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
config.ini
test.py
.vscode/

# Byte-compiled / optimized / DLL files
__pycache__/
Expand Down
38 changes: 38 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
repos:
- repo: https://github.com/pre-commit/pre-commit-hooks
rev: v3.2.0
hooks:
- id: trailing-whitespace
- id: end-of-file-fixer
- id: check-yaml
- id: check-added-large-files
- id: debug-statements
- id: double-quote-string-fixer
- id: name-tests-test
- id: requirements-txt-fixer
- repo: https://github.com/asottile/setup-cfg-fmt
rev: v2.2.0
hooks:
- id: setup-cfg-fmt
- repo: https://github.com/asottile/add-trailing-comma
rev: v2.4.0
hooks:
- id: add-trailing-comma
args: [--py36-plus]
- repo: https://github.com/asottile/pyupgrade
rev: v3.3.1
hooks:
- id: pyupgrade
args: [--py37-plus]
- repo: https://github.com/pre-commit/mirrors-autopep8
rev: v2.0.2
hooks:
- id: autopep8
- repo: https://github.com/PyCQA/flake8
rev: 6.0.0
hooks:
- id: flake8
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.0.1
hooks:
- id: mypy
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,4 @@ Controll all your lights directly from the comfort of your System Tray.

## Usage
Download the [latest release](https://github.com/vincentscode/HomeTray/releases) and run it.
Optionally add it to autoruns.
Optionally add it to autoruns.
38 changes: 27 additions & 11 deletions hometray/__main__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
"""Main entry point for the application"""
from __future__ import annotations

import sys
import wx
import wx.adv
import sys
from homeassistant_api import Client

if getattr(sys, 'frozen', False):
Expand All @@ -9,14 +12,22 @@
from hometray.config import Config
from hometray.settings import Settings
else:
from iconmanager import IconManager
from tray import EntityTrayIcon
from config import Config
from settings import Settings
from iconmanager import IconManager # type:ignore
from tray import EntityTrayIcon # type:ignore
from config import Config # type:ignore
from settings import Settings # type:ignore


class App(wx.App):
def OnInit(self):
"""Main application class"""

frame: wx.Frame
tray_icons: list[EntityTrayIcon]

# pylint: disable=invalid-name
def OnInit(self) -> bool:
"""Called when the application is initialized."""

# init GUI
self.frame = wx.Frame(None)
self.SetTopWindow(self.frame)
Expand All @@ -25,7 +36,7 @@ def OnInit(self):
config = Config.load()
settings = Settings(config)
settings.initial_setup()

# init hass client
client = Client(config.api_url, config.token, cache_session=False)

Expand All @@ -35,7 +46,7 @@ def OnInit(self):
self.tray_icons = []
for domain in config.domains:
for entity in client.get_entities()[domain].entities:
full_id = f"{domain}.{entity}"
full_id = f'{domain}.{entity}'
if full_id in config.domain_entities_ignore or full_id in config.entities:
continue

Expand All @@ -46,16 +57,21 @@ def OnInit(self):

return True

def OnExit(self):
# pylint: disable=invalid-name
def OnExit(self) -> int:
"""Called when the application is exiting."""
for tray_icon in self.tray_icons:
tray_icon.cleanup()

return 0

def main():

def main() -> int:
"""Main entry point for the application"""
app = App(False)
app.MainLoop()
return 0


if __name__ == '__main__':
main()
sys.exit(main())
210 changes: 130 additions & 80 deletions hometray/config.py
Original file line number Diff line number Diff line change
@@ -1,94 +1,144 @@
"""Handles all configuration related tasks."""
from __future__ import annotations

import configparser
from collections.abc import Iterable
from typing import Any
from typing import Callable
from typing import Generic
from typing import get_args
from typing import TypeVar

T = TypeVar('T')


class ConfigProperty(Generic[T]):
"""A generic config property"""

_configured: bool = False
_config_parser: configparser.ConfigParser
_section: str
_key: str
_default: T
_deserialize: Callable[[str], T]
_serialize: Callable[[T], str]
_save: Callable[[], None]

@classmethod
def configure(cls, config_parser: configparser.ConfigParser, section: str, key: str, default: T, deserialize: Callable[[str], T] | None = None, serialize: Callable[[T], str] | None = None, save: Callable[[], None] | None = None) -> ConfigProperty[T]:
"""Generate a confgiured instance of the property"""

def identity(x: T) -> T:
return x

def nop() -> None:
pass

instance = cls()
instance._config_parser = config_parser
instance._section = section
instance._key = key
instance._default = default
instance._deserialize = identity if deserialize is None else deserialize # type: ignore
instance._serialize = identity if serialize is None else serialize # type: ignore
instance._save = nop if save is None else save
return instance

def __get__(self, instance: Any | None, owner: Any | None) -> T:
print('__get__', instance, owner)
if not self._config_parser.has_option(self._section, self._key):
return self._default
return self._deserialize(self._config_parser.get(self._section, self._key))

def __set__(self, instance: Any | None, value: ConfigProperty[Any] | T) -> None:
print('__set__', instance, value, type(self))
if not self._configured:
self.__dict__ = value.__dict__
self._configured = True
else:
self._config_parser.set(self._section, self._key, value=self._serialize(value))
self._save()


class SerializationHelpers:
"""Helper class for serializing and deserializing config values"""

@staticmethod
def deserialize_list(value: str) -> list[str]:
"""Deserialize a comma separated list"""
return list(filter(lambda x: x != '', value.split(',')))

@staticmethod
def serialize_list(value: list[str]) -> str:
"""Serialize a list to a comma separated string"""
return ','.join(value)

@staticmethod
def deserialize_color(value: str) -> list[int]:
return list(map(int, value.split(',')))

@staticmethod
def serialize_color(value: list[int]) -> str:
return ','.join(map(str, value))


class Config:
"""Configuration file wrapper class"""

token: ConfigProperty[str] = ConfigProperty()
api_url: ConfigProperty[str] = ConfigProperty()
entities: ConfigProperty[list[str]] = ConfigProperty()
domains: ConfigProperty[list[str]] = ConfigProperty()
domain_entities_ignore: ConfigProperty[list[str]] = ConfigProperty()
update_interval: ConfigProperty[int] = ConfigProperty()

color_use_rgb_value: ConfigProperty[bool] = ConfigProperty()
color_on: ConfigProperty[list[int]] = ConfigProperty()
color_off: ConfigProperty[list[int]] = ConfigProperty()
color_unknown: ConfigProperty[list[int]] = ConfigProperty()

class Config(object):
def __init__(self, filename: str, section: str, color_section: str):
super(Config, self).__init__()
super().__init__()

self._filename = filename
self._main_section = section
self._color_section = color_section

self._config = configparser.ConfigParser()
self._config.read(self._filename)

if not self._config.has_section(self._main_section):
self._config.add_section(self._main_section)


self._ensure_section_exists(self._main_section)
self._ensure_section_exists(self._color_section)

# main section
self.token = ConfigProperty.configure(self._config, self._main_section, 'Token', None, save=self.save)
self.api_url = ConfigProperty.configure(self._config, self._main_section, 'ApiUrl', None, save=self.save)
self.entities = ConfigProperty.configure(self._config, self._main_section, 'Entities', [], SerializationHelpers.deserialize_list, SerializationHelpers.serialize_list, self.save)
self.domains = ConfigProperty.configure(self._config, self._main_section, 'Domains', [], SerializationHelpers.deserialize_list, SerializationHelpers.serialize_list, self.save)
self.domain_entities_ignore = ConfigProperty.configure(self._config, self._main_section, 'DomainEntitiesIgnore', [], SerializationHelpers.deserialize_list, SerializationHelpers.serialize_list, self.save)
self.update_interval = ConfigProperty.configure(self._config, self._main_section, 'UpdateInterval', 5, int, str, self.save)

# color section
self.color_use_rgb_value = ConfigProperty.configure(self._config, self._color_section, 'UseRGBValue', True, bool, str, self.save)
self.color_on = ConfigProperty.configure(self._config, self._color_section, 'On', [253, 213, 27], SerializationHelpers.deserialize_color, SerializationHelpers.serialize_color, self.save)
self.color_off = ConfigProperty.configure(self._config, self._color_section, 'Off', [225, 225, 225], SerializationHelpers.deserialize_color, SerializationHelpers.serialize_color, self.save)
self.color_unknown = ConfigProperty.configure(self._config, self._color_section, 'Unknown', [100, 100, 100], SerializationHelpers.deserialize_color, SerializationHelpers.serialize_color, self.save)

@classmethod
def load(cls, filename="config.ini", section='HASS', color_section='COLORS') -> "Config":
def load(cls: type[Config], filename: str = 'config.ini', section: str = 'HASS', color_section: str = 'COLORS') -> Config:
"""Load the config from the given file"""
return cls(filename, section, color_section)

@property
def token(self) -> str:
return self._config.get(self._main_section, 'Token', fallback=None)

@token.setter
def token(self, value: str) -> None:
self._config.set(self._main_section, 'Token', value=value)
self._save()

@property
def api_url(self) -> str:
return self._config.get(self._main_section, 'ApiUrl', fallback=None)

@api_url.setter
def api_url(self, value: str) -> None:
self._config.set(self._main_section, 'ApiUrl', value=value)
self._save()

@property
def entities(self) -> list[str]:
entites = self._config.get(self._main_section, 'Entities', fallback="")
return [x for x in entites.split(',') if x != '']

@entities.setter
def entities(self, value: list[str]) -> None:
self._config.set(self._main_section, 'Entities', value=','.join(value))
self._save()

@property
def domains(self) -> list[str]:
domains = self._config.get(self._main_section, 'Domains', fallback="")
return [x for x in domains.split(',') if x != '']

@domains.setter
def domains(self, value: list[str]) -> None:
self._config.set(self._main_section, 'Domains', value=','.join(value))
self._save()

@property
def domain_entities_ignore(self) -> list[str]:
domain_entities_ignore = self._config.get(self._main_section, 'DomainEntitiesIgnore', fallback="")
return [x for x in domain_entities_ignore.split(',') if x != '']

@domain_entities_ignore.setter
def domain_entities_ignore(self, value: list[str]) -> None:
self._config.set(self._main_section, 'DomainEntitiesIgnore', value=','.join(value))
self._save()

@property
def update_interval(self) -> list[int]:
return self._config.getint(self._main_section, 'UpdateInterval', fallback=5)

@property
def color_use_rgb_value(self) -> list[int]:
return self._config.getboolean(self._color_section, 'UseRGBValue', fallback=True)

@property
def color_on(self) -> list[int]:
default = [253, 213, 27]
return [int(x) for x in self._config.get(self._color_section, 'On', fallback=','.join(map(lambda x: str(x), default))).split(",")]

@property
def color_off(self) -> list[int]:
default = [225, 225, 225]
return [int(x) for x in self._config.get(self._color_section, 'Off', fallback=','.join(map(lambda x: str(x), default))).split(",")]

@property
def color_unknown(self) -> list[int]:
default = [100, 100, 100]
return [int(x) for x in self._config.get(self._color_section, 'Unknown', fallback=','.join(map(lambda x: str(x), default))).split(",")]

def _save(self) -> None:
with open(self._filename, 'w') as configfile:
def save(self) -> None:
with open(self._filename, 'w', encoding='utf8') as configfile:
self._config.write(configfile)

def _ensure_section_exists(self, section_name: str) -> None:
if not self._config.has_section(section_name):
self._config.add_section(section_name)


if __name__ == '__main__':
c = Config.load()
print(c.token)
print(c.color_on)
Loading

0 comments on commit b9ae808

Please sign in to comment.