CRUD store for named scenes, backed by a JSON file.
| 155 | |
| 156 | |
| 157 | class SceneStore: |
| 158 | """CRUD store for named scenes, backed by a JSON file.""" |
| 159 | |
| 160 | def __init__(self, path: Path | None = None) -> None: |
| 161 | self._path = path or _SCENES_FILE |
| 162 | self._scenes: dict[str, SceneConfig] = {} |
| 163 | self._load() |
| 164 | |
| 165 | def _load(self) -> None: |
| 166 | """Load scenes from disk. Silently ignores corrupt files.""" |
| 167 | self._scenes = {} |
| 168 | try: |
| 169 | if self._path.exists(): |
| 170 | raw = json.loads(self._path.read_text()) |
| 171 | if isinstance(raw, dict): |
| 172 | scenes_data = raw.get("scenes", {}) |
| 173 | if isinstance(scenes_data, dict): |
| 174 | for _key, scene_data in scenes_data.items(): |
| 175 | scene = _deserialize_scene(scene_data) |
| 176 | if scene: |
| 177 | self._scenes[scene.name] = scene |
| 178 | except Exception: |
| 179 | logger.warning("Failed to load scenes from %s", self._path, exc_info=True) |
| 180 | |
| 181 | def _save(self) -> None: |
| 182 | """Persist scenes to disk.""" |
| 183 | try: |
| 184 | self._path.parent.mkdir(parents=True, exist_ok=True, mode=0o700) |
| 185 | data = { |
| 186 | "version": "1.0", |
| 187 | "scenes": { |
| 188 | name: _serialize_scene(scene) |
| 189 | for name, scene in sorted(self._scenes.items()) |
| 190 | }, |
| 191 | } |
| 192 | self._path.write_text(json.dumps(data, indent=2, sort_keys=True)) |
| 193 | self._path.chmod(0o600) |
| 194 | except Exception: |
| 195 | logger.error("Failed to save scenes to %s", self._path, exc_info=True) |
| 196 | |
| 197 | def get(self, name: str) -> SceneConfig | None: |
| 198 | """Look up a scene by name (case-insensitive).""" |
| 199 | return self._scenes.get(name.strip().lower()) |
| 200 | |
| 201 | def list(self) -> list[SceneConfig]: |
| 202 | """Return all scenes, sorted by name.""" |
| 203 | return sorted(self._scenes.values(), key=lambda s: s.name) |
| 204 | |
| 205 | def add(self, scene: SceneConfig) -> SceneConfig: |
| 206 | """Add or update a scene. Returns the stored scene.""" |
| 207 | normalized = SceneConfig( |
| 208 | name=scene.name.strip().lower(), |
| 209 | primary=scene.primary.strip(), |
| 210 | fallback=[f.strip() for f in scene.fallback if f.strip()], |
| 211 | hard_pin=scene.hard_pin, |
| 212 | description=scene.description, |
| 213 | tier_floor=scene.tier_floor, |
| 214 | tier_cap=scene.tier_cap, |
no outgoing calls