Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: automatically migrate old configuration file #14

Merged
merged 2 commits into from
Mar 8, 2024
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
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

_Changes in the next release_

### Fixed
- Automatically migrate old configuration file at startup and configuration flow, otherwise the device must be paired again.

---

## v0.13.1 - 2024-03-07
Expand Down
40 changes: 38 additions & 2 deletions intg-appletv/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
:license: Mozilla Public License Version 2.0, see LICENSE for more details.
"""

import asyncio
import dataclasses
import json
import logging
Expand All @@ -13,6 +14,8 @@
from enum import Enum
from typing import Iterator

import discover

_LOG = logging.getLogger(__name__)

_CFG_FILENAME = "config.json"
Expand Down Expand Up @@ -160,14 +163,47 @@ def load(self) -> bool:
with open(self._cfg_file_path, "r", encoding="utf-8") as f:
data = json.load(f)
for item in data:
self._config.append(AtvDevice(**item))
# not using AtvDevice(**item) to be able to migrate old configuration files with missing attributes
atv = AtvDevice(
item.get("identifier"), item.get("name", ""), item.get("credentials"), item.get("address")
)
self._config.append(atv)
return True
except OSError as err:
_LOG.error("Cannot open the config file: %s", err)
except (ValueError, TypeError) as err:
except (AttributeError, ValueError, TypeError) as err:
_LOG.error("Empty or invalid config file: %s", err)

return False

def migration_required(self) -> bool:
"""Check if configuration migration is required."""
for item in self._config:
if not item.name:
return True
return False

async def migrate(self) -> bool:
"""Migrate configuration if required."""
result = True
for item in self._config:
if not item.name:
_LOG.info("Migrating configuration: scanning for device %s to update device name", item.identifier)
search_hosts = [item.address] if item.address else None
discovered_atvs = await discover.apple_tvs(
asyncio.get_event_loop(), identifier=item.identifier, hosts=search_hosts
)
if discovered_atvs:
item.name = discovered_atvs[0].name
_LOG.info("Updating device configuration %s with name: %s", item.identifier, item.name)
if not self.store():
result = False
else:
result = False
_LOG.warning(
"Could not migrate device configuration %s: device not found on network", item.identifier
)
return result


devices: Devices | None = None
6 changes: 4 additions & 2 deletions intg-appletv/discover.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,9 @@
_LOG = logging.getLogger(__name__)


async def apple_tvs(loop: AbstractEventLoop, hosts: list[str] | None = None) -> list[pyatv.interface.BaseConfig]:
async def apple_tvs(
loop: AbstractEventLoop, identifier: str | set[str] | None = None, hosts: list[str] | None = None
) -> list[pyatv.interface.BaseConfig]:
"""Discover Apple TVs on the network using pyatv.scan."""
if hosts:
_LOG.info("Connecting to %s", hosts)
Expand All @@ -24,7 +26,7 @@ async def apple_tvs(loop: AbstractEventLoop, hosts: list[str] | None = None) ->

# extra safety, if anything goes wrong here the reconnection logic is dead
try:
atvs = await pyatv.scan(loop, hosts=hosts)
atvs = await pyatv.scan(loop, identifier=identifier, hosts=hosts)
res = []

for tv in atvs:
Expand Down
3 changes: 3 additions & 0 deletions intg-appletv/driver.py
Original file line number Diff line number Diff line change
Expand Up @@ -544,13 +544,16 @@ async def main():
level = os.getenv("UC_LOG_LEVEL", "DEBUG").upper()
logging.getLogger("tv").setLevel(level)
logging.getLogger("driver").setLevel(level)
logging.getLogger("config").setLevel(level)
logging.getLogger("discover").setLevel(level)
logging.getLogger("setup_flow").setLevel(level)

# logging.getLogger("pyatv").setLevel(logging.DEBUG)

# load paired devices
config.devices = config.Devices(api.config_dir_path, on_device_added, on_device_removed)
# best effort migration (if required): network might not be available during startup
await config.devices.migrate()
# and register them as available devices.
# Note: device will be moved to configured devices with the subscribe_events request!
# This will also start the device connection.
Expand Down
4 changes: 4 additions & 0 deletions intg-appletv/setup_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,6 +143,10 @@ async def _handle_driver_setup(msg: DriverSetupRequest) -> RequestUserInput | Se
if reconfigure:
_setup_step = SetupSteps.CONFIGURATION_MODE

# make sure configuration is up-to-date
if config.devices.migration_required():
await config.devices.migrate()

# get all configured devices for the user to choose from
dropdown_devices = []
for device in config.devices.all():
Expand Down
6 changes: 5 additions & 1 deletion intg-appletv/tv.py
Original file line number Diff line number Diff line change
Expand Up @@ -160,7 +160,7 @@ def identifier(self) -> str:
@property
def log_id(self) -> str:
"""Return a log identifier."""
return self._device.name
return self._device.name if self._device.name else self._device.identifier

@property
def name(self) -> str:
Expand Down Expand Up @@ -392,6 +392,10 @@ async def _connect(self, conf: pyatv.interface.BaseConfig) -> None:
)

_LOG.debug("[%s] Connecting to device", conf.name)
# In case the device has been renamed
if self._device.name != conf.name:
self._device.name = conf.name

self._atv = await pyatv.connect(conf, self._loop)

async def disconnect(self) -> None:
Expand Down