Skip to content

Commit

Permalink
Implemented saving/loading of named Presets
Browse files Browse the repository at this point in the history
  • Loading branch information
ejohb committed Jan 1, 2023
1 parent 6b1aab0 commit e540f23
Show file tree
Hide file tree
Showing 5 changed files with 242 additions and 108 deletions.
92 changes: 68 additions & 24 deletions amniotic/audio.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
import logging
import vlc
from cachetools.func import ttl_cache
from copy import deepcopy
from datetime import datetime
from itertools import cycle
from numbers import Number
Expand Down Expand Up @@ -97,7 +98,7 @@ class Amniotic:
VOLUME_DEFAULT = 50
THEME_NAME_DEFAULT = 'Default Theme'

def __init__(self, path: Union[Path, str], device_names: Optional[dict[str, str]] = None):
def __init__(self, path: Union[Path, str], device_names: Optional[dict[str, str]] = None, presets: Dict = None):
"""
Read audio directories and instantiate Theme objects
Expand Down Expand Up @@ -132,6 +133,9 @@ def __init__(self, path: Union[Path, str], device_names: Optional[dict[str, str]
self.volume = 0
self.set_volume(self.VOLUME_DEFAULT)

self.presets = deepcopy(presets) or {} # Ensure changes to Presets don't also affect config.presets
self.preset_current = None

@property
def devices(self) -> dict[str, str]:
"""
Expand Down Expand Up @@ -245,7 +249,19 @@ def status_text(self) -> str:
status_text = ', '.join(statuses_themes) or None
return status_text

def get_preset(self) -> Dict:
def get_preset(self) -> Optional[str]:
"""
Get the currently applied preset name, if it still matches the current settings
"""
preset_data_current = self.get_preset_data()
preset_data = self.presets.get(self.preset_current)
if preset_data != preset_data_current:
self.preset_current = None
return self.preset_current

def get_preset_data(self) -> Dict:
"""
Get Preset representing current settings, namely, Master and Theme volumes
Expand All @@ -260,7 +276,20 @@ def get_preset(self) -> Dict:
}
return preset

def apply_preset(self, preset: Dict):
def apply_preset(self, name: str):
"""
Apply the Preset matching the specified name, if it exists
"""
if name not in self.presets:
msg = f'Preset "{name}" does not exist'
logging.warning(msg)
return
self.preset_current = name
self.apply_preset_data(self.presets[name])

def apply_preset_data(self, preset: Dict):
"""
Apply a preset. Themes that appear in the Preset are implicitly enabled. Non-existent Themes need to be
Expand Down Expand Up @@ -291,6 +320,15 @@ def apply_preset(self, preset: Dict):
theme = self.themes[name]
theme.enabled = False

def add_preset(self, name: str):
"""
Add a new Preset, under the specified name, consisting of the current state
"""
self.presets[name] = self.get_preset_data()
self.apply_preset(name)

def close(self):
"""
Expand Down Expand Up @@ -425,6 +463,7 @@ def set_device(self, device: Optional[str]):
Set the output audio device from its ID. Also handle when that device had been unplugged, etc.
"""
device_old = device
devices = self.devices

if device not in devices:
Expand All @@ -433,7 +472,7 @@ def set_device(self, device: Optional[str]):
if device not in devices:
self.enabled = False
device = next(iter(devices or {None}))
msg = f'Current device "{self.device}" no longer available for theme "{self.name}". ' \
msg = f'Current device "{device_old}" no longer available for theme "{self.name}". ' \
f'Defaulting to "{device}". Theme will be disabled.'
logging.warning(msg)

Expand Down Expand Up @@ -484,7 +523,6 @@ def stop(self):
When Theme is stopped, unload its players
"""

for player in self.players or []:
unload_player(player)
msg = f'Theme "{self.name}" unloaded player: {player}'
Expand Down Expand Up @@ -557,6 +595,31 @@ def set_volume_scaled(self, volume_scaled):
if self.enabled:
self.player.audio_set_volume(volume_scaled)

def get_preset(self) -> Dict:
"""
Get preset data
"""
return {'volume': self.volume, 'device': self.device}

def apply_preset(self, preset: Dict):
"""
Apply preset data. Only enabled Themes are mentioned in Presets, so always enable
"""

logging.info(f'Theme "{self.name}" applying preset: {repr(preset)}')
if (volume := preset.get('volume')) is not None:
self.set_volume(volume)

device = preset.get('device')
self.set_device(device)

if device in self.devices:
self.enabled = True

@property
def status(self):
"""
Expand Down Expand Up @@ -609,23 +672,4 @@ def status_text(self):
"""
return f'{self.name} @ {self.volume}%'

def get_preset(self) -> Dict:
"""
Get preset data
"""
return {'volume': self.volume}

def apply_preset(self, preset: Dict):
"""
Apply preset data. Only enabled Themes are mentioned in Presets, so always enable
"""

logging.info(f'Theme "{self.name}" applying preset: {repr(preset)}')
if (volume := preset.get('volume')) is not None:
self.set_volume(volume)

self.enabled = True
9 changes: 5 additions & 4 deletions amniotic/config.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
from os import getenv

import logging
import tempfile
import yaml
from _socket import gethostname
from appdirs import AppDirs
Expand All @@ -18,9 +17,7 @@
MAC_ADDRESS = getmac.get_mac_address().replace(':', '')
HOSTNAME = gethostname()
IS_ADDON = bool(strtobool(getenv(f'{NAME}_IS_ADDON'.upper(), 'false')))
PATH_TEMP_DIR = Path(tempfile.gettempdir())
PATH_LAST_PRESET = PATH_TEMP_DIR / 'amniotic_last_preset.json'

PRESET_LAST_KEY = '.LAST'

@dataclass
class Config:
Expand All @@ -36,8 +33,10 @@ class Config:
device_names: dict = None
logging: str = None
tele_period: int = 300
presets: dict = field(default_factory=dict)
config_raw: dict = field(default_factory=dict)


def __post_init__(self):
path_audio = Path(self.path_audio).absolute()
if not path_audio.exists():
Expand Down Expand Up @@ -104,4 +103,6 @@ def write(self):
"""
config_str = yaml.dump(self.config_raw)
path = self.get_path_config()
logging.info(f'Wrote out config file to: {path}')

return path.write_text(config_str)
101 changes: 93 additions & 8 deletions amniotic/mqtt/control.py
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ class Entity:
HA_PLATFORM = None
NAME = None
ICON_SUFFIX = None
NA_VALUE = '-'
value = None

TEST_ALWAYS_UPDATE = False
Expand Down Expand Up @@ -671,8 +672,8 @@ def update_sensor(self):
Get the sensor for displaying update messages
"""
from amniotic.mqtt.sensor import DownloaderStatus
update_status = self.loop.entities[DownloaderStatus]
from amniotic.mqtt.sensor import DownloadStatus
update_status = self.loop.entities[DownloadStatus]
return update_status

def progress_callback(self, stream: Stream, chunk: bytes, bytes_remaining: int):
Expand Down Expand Up @@ -747,32 +748,116 @@ def handle_incoming(self, value: Any):
threading.Thread(target=self.do_download, args=[value]).start()


class Preset(TextInput):
class PresetData(TextInput):
"""
Home Assistant text input box for io of Presets
Home Assistant text input box for I/O of Presets
"""
ICON_SUFFIX = 'cog-play'
NAME = 'Preset'
NAME = 'Preset Data'

def update_sensor(self):
pass

def set_value(self, value) -> Any:
"""
Apply preset
Apply preset data
"""
self.amniotic.apply_preset(value)
self.amniotic.apply_preset_data(value)

def get_value(self) -> str:
"""
Current settings as Preset data
"""
preset = self.amniotic.get_preset()
preset = self.amniotic.get_preset_data()
preset_json = json.dumps(preset)
return preset_json


class SavePreset(TextInput):
"""
Home Assistant text input box for naming/saving a new Preset
"""
ICON_SUFFIX = 'content-save-plus'
NAME = 'Save Preset As'
PATTERN = '^[^\.].*$'

def update_sensor(self):
pass

@property
def data(self):
"""
Home Assistant announce data for the entity. Don't allow Preset names to start with a '.' because these will be
used internally as special cases (e.g. last-used Preset)
"""
data = super().data | {
# 'pattern': self.PATTERN TODO: Fix this
}
return data

def set_value(self, value):
"""
Add a new Preset
"""
self.amniotic.add_preset(value)
config = self.loop.config
config.config_raw['presets'] = self.amniotic.presets
config.write()

def get_value(self) -> Optional[str]:
"""
We can leave the text as the last value the user entered, so no need to return anything here
"""
return None


class Preset(Select):
"""
Home Assistant Presets selector.
"""
ICON_SUFFIX = 'window-shutter-cog'
NAME = 'Preset'
NA_VALUE = ""

def get_value(self) -> str:
"""
Get the currently selected Preset, if there is one
"""
return self.amniotic.get_preset() or self.NA_VALUE

def set_value(self, value) -> str:
"""
If selector isn't empty, apply the selected Preset
"""
if value == self.NA_VALUE:
return
self.amniotic.apply_preset(value)

def get_options(self, amniotic: Amniotic) -> list[str]:
"""
Get state of the entity, i.e. the list of options and the currently selected option. Also allow no entry to
be selected
"""
return [self.NA_VALUE] + sorted(amniotic.presets.keys())
Loading

0 comments on commit e540f23

Please sign in to comment.