Skip to content

Commit

Permalink
feat: get a family (#73)
Browse files Browse the repository at this point in the history
Co-authored-by: Rob Beal <[email protected]>
Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
Co-authored-by: cdnninja <[email protected]>
  • Loading branch information
4 people authored Jun 23, 2024
1 parent aad9454 commit fc57488
Show file tree
Hide file tree
Showing 5 changed files with 96 additions and 17 deletions.
25 changes: 19 additions & 6 deletions tests/YotoAPI_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,12 @@
from datetime import datetime


class ValidLogin(unittest.TestCase):
class login(unittest.TestCase):
@classmethod
def setUpClass(cls):
load_dotenv()
username = os.getenv("USERNAME")
password = os.getenv("PASSWORD")
api = YotoAPI()
cls.token = api.login(username, password)

cls.token = YotoAPI().login(os.getenv("USERNAME"), os.getenv("PASSWORD"))

def test_access_token(self):
self.assertIsNotNone(self.token.access_token)
Expand All @@ -31,7 +29,7 @@ def test_valid_until_is_greater_than_now(self):
self.assertGreater(self.token.valid_until, datetime.now(pytz.utc))


class InvalidLogin(unittest.TestCase):
class login_invalid(unittest.TestCase):
def test_it_throws_an_error(self):
api = YotoAPI()

Expand All @@ -41,5 +39,20 @@ def test_it_throws_an_error(self):
self.assertEqual(str(error.exception), "Wrong email or password.")


class get_family(unittest.TestCase):
@classmethod
def setUpClass(cls):
load_dotenv()
api = YotoAPI()
token = api.login(os.getenv("USERNAME"), os.getenv("PASSWORD"))
cls.family = api.get_family(token)

def test_it_has_members(self):
self.assertIsNotNone(self.family.members)

def test_it_has_devices(self):
self.assertIsNotNone(self.family.devices)


if __name__ == "__main__":
unittest.main()
60 changes: 60 additions & 0 deletions yoto_api/Family.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
from dataclasses import dataclass
from datetime import datetime
from typing import List, Dict


@dataclass
class Device:
deviceId: str
addedAt: datetime


@dataclass
class Member:
createdAt: datetime
email: str
emailVerified: bool
lastIp: str
lastLoginAt: datetime
loginsCount: int
picture: str
userId: str
addedAt: datetime


@dataclass
class Family:
familyId: str
createdAt: datetime
devices: List[Device]
members: List[Member]
country: str

def __init__(self, data: Dict) -> None:
self.familyId = data["familyId"]
self.createdAt = datetime.fromisoformat(data["createdAt"].rstrip("Z"))

self.devices = [
Device(
deviceId=device["deviceId"],
addedAt=datetime.fromisoformat(device["addedAt"].rstrip("Z")),
)
for device in data["devices"]
]

self.members = [
Member(
createdAt=datetime.fromisoformat(member["createdAt"].rstrip("Z")),
email=member["email"],
emailVerified=member["emailVerified"],
lastIp=member["lastIp"],
lastLoginAt=datetime.fromisoformat(member["lastLoginAt"].rstrip("Z")),
loginsCount=member["loginsCount"],
picture=member["picture"],
userId=member["userId"],
addedAt=datetime.fromisoformat(member["addedAt"].rstrip("Z")),
)
for member in data["members"]
]

self.country = data["country"]
21 changes: 10 additions & 11 deletions yoto_api/YotoAPI.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from .const import DOMAIN, POWER_SOURCE
from .Token import Token
from .Card import Card, Chapter, Track
from .Family import Family
from .YotoPlayer import YotoPlayer, YotoPlayerConfig
from .utils import get_child_value

Expand Down Expand Up @@ -78,6 +79,14 @@ def refresh_token(self, token: Token) -> Token:
valid_until=valid_until,
)

def get_family(self, token: Token) -> dict:
url = self.BASE_URL + "/user/family"
headers = self._get_authenticated_headers(token)
response = requests.get(url, headers=headers).json()

_LOGGER.debug(f"{DOMAIN} - Get Family Response: {response}")
return Family(response["family"])

def update_players(self, token: Token, players: list[YotoPlayer]) -> None:
response = self._get_devices(token)
for item in response["devices"]:
Expand Down Expand Up @@ -253,7 +262,7 @@ def update_card_detail(self, token: Token, card: Card) -> None:
)

def set_player_config(self, token: Token, player_id: str, config: YotoPlayerConfig):
url = self.BASE_URL + "/device-v2/" + player_id + "/config"
url = f"{self.BASE_URL}/device-v2/{player_id}/config"
config_payload = {}
if config.day_mode_time:
config_payload["dayTime"] = config.day_mode_time.strftime("%H:%M")
Expand Down Expand Up @@ -308,16 +317,6 @@ def _get_device_config(self, token: Token, player_id: str) -> None:
return response
# 2024-05-15 17:25:48,604 yoto_api.YotoAPI DEBUG:yoto_api - Get Device Config Response: {'device': {'deviceId': 'y23IBS76kCaOSrGlz29XhIFO', 'name': '', 'errorCode': None, 'fwVersion': 'v2.17.5-5', 'popCode': 'FAJKEH', 'releaseChannelId': 'prerelease', 'releaseChannelVersion': 'v2.17.5-5', 'activationPopCode': 'IBSKCAAA', 'registrationCode': 'IBSKCAAA', 'deviceType': 'v3', 'deviceFamily': 'v3', 'deviceGroup': '', 'mac': 'b4:8a:0a:92:7a:f4', 'online': True, 'geoTimezone': 'America/Edmonton', 'getPosix': 'MST7MDT,M3.2.0,M11.1.0', 'status': {'activeCard': 'none', 'aliveTime': None, 'als': 0, 'battery': None, 'batteryLevel': 100, 'batteryRemaining': None, 'bgDownload': 0, 'bluetoothHp': 0, 'buzzErrors': 0, 'bytesPS': 0, 'cardInserted': 0, 'chgStatLevel': None, 'charging': 0, 'day': 1, 'dayBright': None, 'dbatTimeout': None, 'dnowBrightness': None, 'deviceId': 'y23IBS76kCaOSrGlz29XhIFO', 'errorsLogged': 164, 'failData': None, 'failReason': None, 'free': None, 'free32': None, 'freeDisk': 30219824, 'freeDMA': None, 'fwVersion': 'v2.17.5-5', 'headphones': 0, 'lastSeenAt': None, 'missedLogs': None, 'nfcErrs': 'n/a', 'nightBright': None, 'nightlightMode': '0x194a55', 'playingStatus': 0, 'powerCaps': '0x02', 'powerSrc': 2, 'qiOtp': None, 'sd_info': None, 'shutDown': None, 'shutdownTimeout': None, 'ssid': 'speed', 'statusVersion': None, 'temp': '0:24', 'timeFormat': None, 'totalDisk': 31385600, 'twdt': 0, 'updatedAt': '2024-05-15T23:23:45.284Z', 'upTime': 159925, 'userVolume': 31, 'utcOffset': -21600, 'utcTime': 1715815424, 'volume': 34, 'wifiRestarts': None, 'wifiStrength': -54}, 'config': {'locale': 'en', 'bluetoothEnabled': '1', 'repeatAll': True, 'showDiagnostics': True, 'btHeadphonesEnabled': True, 'pauseVolumeDown': False, 'pausePowerButton': True, 'displayDimTimeout': '60', 'shutdownTimeout': '3600', 'headphonesVolumeLimited': False, 'dayTime': '06:30', 'maxVolumeLimit': '16', 'ambientColour': '#40bfd9', 'dayDisplayBrightness': 'auto', 'dayYotoDaily': '3nC80/daily/<yyyymmdd>', 'dayYotoRadio': '3nC80/radio-day/01', 'daySoundsOff': '0', 'nightTime': '18:20', 'nightMaxVolumeLimit': '8', 'nightAmbientColour': '#f57399', 'nightDisplayBrightness': '100', 'nightYotoDaily': '0', 'nightYotoRadio': '0', 'nightSoundsOff': '1', 'hourFormat': '12', 'timezone': '', 'displayDimBrightness': '0', 'systemVolume': '87', 'volumeLevel': 'safe', 'clockFace': 'digital-sun', 'logLevel': 'none', 'alarms': []}, 'shortcuts': {'versionId': '36645a9463e038d6cb9923257b38d9d9df7a6509', 'modes': {'day': {'content': [{'cmd': 'track-play', 'params': {'card': '3nC80', 'chapter': 'daily', 'track': '<yyyymmdd>'}}, {'cmd': 'track-play', 'params': {'card': '3nC80', 'chapter': 'radio-day', 'track': '01'}}]}, 'night': {'content': [{'cmd': 'track-play', 'params': {'card': '3nC80', 'chapter': 'daily', 'track': '<yyyymmdd>'}}, {'cmd': 'track-play', 'params': {'card': '3nC80', 'chapter': 'radio-night', 'track': '01'}}]}}}}}

def _get_device_config(self, token: Token, player_id: str) -> None:
url = self.BASE_URL + "/device-v2/" + player_id + "/config"

headers = self._get_authenticated_headers(token)

response = requests.get(url, headers=headers).json()
_LOGGER.debug(f"{DOMAIN} - Get Device Config Response: {response}")
return response
# 2024-05-15 17:25:48,604 yoto_api.YotoAPI DEBUG:yoto_api - Get Device Config Response: {'device': {'deviceId': 'y23IBS76kCaOSrGlz29XhIFO', 'name': '', 'errorCode': None, 'fwVersion': 'v2.17.5-5', 'popCode': 'FAJKEH', 'releaseChannelId': 'prerelease', 'releaseChannelVersion': 'v2.17.5-5', 'activationPopCode': 'IBSKCAAA', 'registrationCode': 'IBSKCAAA', 'deviceType': 'v3', 'deviceFamily': 'v3', 'deviceGroup': '', 'mac': 'b4:8a:0a:92:7a:f4', 'online': True, 'geoTimezone': 'America/Edmonton', 'getPosix': 'MST7MDT,M3.2.0,M11.1.0', 'status': {'activeCard': 'none', 'aliveTime': None, 'als': 0, 'battery': None, 'batteryLevel': 100, 'batteryRemaining': None, 'bgDownload': 0, 'bluetoothHp': 0, 'buzzErrors': 0, 'bytesPS': 0, 'cardInserted': 0, 'chgStatLevel': None, 'charging': 0, 'day': 1, 'dayBright': None, 'dbatTimeout': None, 'dnowBrightness': None, 'deviceId': 'y23IBS76kCaOSrGlz29XhIFO', 'errorsLogged': 164, 'failData': None, 'failReason': None, 'free': None, 'free32': None, 'freeDisk': 30219824, 'freeDMA': None, 'fwVersion': 'v2.17.5-5', 'headphones': 0, 'lastSeenAt': None, 'missedLogs': None, 'nfcErrs': 'n/a', 'nightBright': None, 'nightlightMode': '0x194a55', 'playingStatus': 0, 'powerCaps': '0x02', 'powerSrc': 2, 'qiOtp': None, 'sd_info': None, 'shutDown': None, 'shutdownTimeout': None, 'ssid': 'speed', 'statusVersion': None, 'temp': '0:24', 'timeFormat': None, 'totalDisk': 31385600, 'twdt': 0, 'updatedAt': '2024-05-15T23:23:45.284Z', 'upTime': 159925, 'userVolume': 31, 'utcOffset': -21600, 'utcTime': 1715815424, 'volume': 34, 'wifiRestarts': None, 'wifiStrength': -54}, 'config': {'locale': 'en', 'bluetoothEnabled': '1', 'repeatAll': True, 'showDiagnostics': True, 'btHeadphonesEnabled': True, 'pauseVolumeDown': False, 'pausePowerButton': True, 'displayDimTimeout': '60', 'shutdownTimeout': '3600', 'headphonesVolumeLimited': False, 'dayTime': '06:30', 'maxVolumeLimit': '16', 'ambientColour': '#40bfd9', 'dayDisplayBrightness': 'auto', 'dayYotoDaily': '3nC80/daily/<yyyymmdd>', 'dayYotoRadio': '3nC80/radio-day/01', 'daySoundsOff': '0', 'nightTime': '18:20', 'nightMaxVolumeLimit': '8', 'nightAmbientColour': '#f57399', 'nightDisplayBrightness': '100', 'nightYotoDaily': '0', 'nightYotoRadio': '0', 'nightSoundsOff': '1', 'hourFormat': '12', 'timezone': '', 'displayDimBrightness': '0', 'systemVolume': '87', 'volumeLevel': 'safe', 'clockFace': 'digital-sun', 'logLevel': 'none', 'alarms': []}, 'shortcuts': {'versionId': '36645a9463e038d6cb9923257b38d9d9df7a6509', 'modes': {'day': {'content': [{'cmd': 'track-play', 'params': {'card': '3nC80', 'chapter': 'daily', 'track': '<yyyymmdd>'}}, {'cmd': 'track-play', 'params': {'card': '3nC80', 'chapter': 'radio-day', 'track': '01'}}]}, 'night': {'content': [{'cmd': 'track-play', 'params': {'card': '3nC80', 'chapter': 'daily', 'track': '<yyyymmdd>'}}, {'cmd': 'track-play', 'params': {'card': '3nC80', 'chapter': 'radio-night', 'track': '01'}}]}}}}}

def _get_cards(self, token: Token) -> dict:
############## ${BASE_URL}/card/family/library #############
url = self.BASE_URL + "/card/family/library"
Expand Down
6 changes: 6 additions & 0 deletions yoto_api/YotoManager.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@

from .YotoAPI import YotoAPI
from .YotoMQTTClient import YotoMQTTClient
from .Family import Family
from .Token import Token
from .const import DOMAIN
from .YotoPlayer import YotoPlayerConfig
Expand All @@ -24,6 +25,7 @@ def __init__(self, username: str, password: str) -> None:
self.library: dict = {}
self.mqtt_client: YotoMQTTClient = None
self.callback: None
self.family: Family = None

def initialize(self) -> None:
self.token: Token = self.api.login(self.username, self.password)
Expand Down Expand Up @@ -55,6 +57,10 @@ def update_library(self) -> None:
# Updates library and all card data. Typically only required on startup.
self.api.update_library(self.token, self.library)

def update_family(self) -> None:
# Updates the family object with family details
self.family = self.api.get_family(self.token)

def update_card_detail(self, cardId: str) -> None:
# Used to get more details for a specific card. update_cards must be run first to get the basic library details. Could be called in a loop for all cards but this is a lot of API calls when the data may not be needed.
if cardId not in self.library:
Expand Down
1 change: 1 addition & 0 deletions yoto_api/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
# flake8: noqa

from .YotoPlayer import YotoPlayer, YotoPlayerConfig
from .Family import Family
from .YotoManager import YotoManager
from .YotoAPI import YotoAPI
from .Token import Token
Expand Down

0 comments on commit fc57488

Please sign in to comment.