Skip to content

Commit

Permalink
Merge pull request #31 from SeraphimSerapis/updates_seraphimserapis
Browse files Browse the repository at this point in the history
* Adds unique_id for support of UI customization (fixes #30)
* Implements device state attributes (fixes #29)
  • Loading branch information
snicker authored Feb 21, 2022
2 parents f18f6f7 + 92ef679 commit c61a623
Show file tree
Hide file tree
Showing 3 changed files with 92 additions and 56 deletions.
2 changes: 1 addition & 1 deletion custom_components/zwift/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
"documentation": "https://github.com/snicker/zwift_hass/blob/master/README.md",
"requirements": ["zwift-client==0.2.0"],
"dependencies": [],
"version": "3.2.6",
"version": "3.3",
"codeowners": ["@snicker"],
"iot_class": "cloud_polling"
}
139 changes: 87 additions & 52 deletions custom_components/zwift/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,17 @@
"""

from homeassistant.helpers.event import async_call_later
from homeassistant.helpers.dispatcher import dispatcher_send, \
async_dispatcher_connect
from homeassistant.core import callback
from homeassistant.helpers.entity import Entity
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.aiohttp_client import SERVER_SOFTWARE
from homeassistant.const import CONF_NAME, CONF_USERNAME, CONF_PASSWORD, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP
from homeassistant.components.sensor import PLATFORM_SCHEMA
from datetime import timedelta
import voluptuous as vol
import logging
import threading
import time
Expand All @@ -23,21 +34,10 @@

REQUIREMENTS = ['zwift-client==0.2.0']

import voluptuous as vol
from datetime import timedelta
from homeassistant.components.sensor import PLATFORM_SCHEMA
try:
from homeassistant.components.binary_sensor import BinarySensorEntity
except ImportError:
from homeassistant.components.binary_sensor import BinarySensorDevice as BinarySensorEntity
from homeassistant.const import CONF_NAME, CONF_USERNAME, CONF_PASSWORD, EVENT_HOMEASSISTANT_START, EVENT_HOMEASSISTANT_STOP
from homeassistant.helpers.aiohttp_client import SERVER_SOFTWARE
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity import Entity
from homeassistant.core import callback
from homeassistant.helpers.dispatcher import dispatcher_send, \
async_dispatcher_connect
from homeassistant.helpers.event import async_call_later

CONF_UPDATE_INTERVAL = 'update_interval'
CONF_PLAYERS = 'players'
Expand Down Expand Up @@ -108,6 +108,7 @@
'runprogress': {'name': 'Run Progress', 'unit': '%', 'icon': 'mdi:transfer-right'},
}


async def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
"""Set up the Zwift sensor."""

Expand All @@ -118,12 +119,12 @@ async def async_setup_platform(hass, config, async_add_entities, discovery_info=
update_interval = config.get(CONF_UPDATE_INTERVAL)
include_self = config.get(CONF_INCLUDE_SELF)


zwift_data = ZwiftData(update_interval, username, password, players, hass)
try:
await zwift_data._connect()
except:
_LOGGER.exception("Could not create Zwift sensor named '{}'!".format(name))
_LOGGER.exception(
"Could not create Zwift sensor named '{}'!".format(name))
return

if include_self:
Expand All @@ -150,9 +151,11 @@ async def update_data(now):
for player_id in zwift_data.players:
for variable in SENSOR_TYPES:
if SENSOR_TYPES[variable].get('binary'):
dev.append(ZwiftBinarySensorDevice(name, zwift_data, zwift_data.players[player_id], variable))
dev.append(ZwiftBinarySensorDevice(name, zwift_data,
zwift_data.players[player_id], variable))
else:
dev.append(ZwiftSensorDevice(name, zwift_data, zwift_data.players[player_id], variable))
dev.append(ZwiftSensorDevice(name, zwift_data,
zwift_data.players[player_id], variable))

async_add_entities(dev, True)

Expand All @@ -166,19 +169,26 @@ def __init__(self, name, zwift_data, player, sensor_type):
self._type = sensor_type
self._state = None
self._attrs = {}
self._unique_id = "{}_{}_{}".format(self._base_name, SENSOR_TYPES[self._type].get(
'name'), self._player.player_id).replace(" ", "").lower()

@property
def unique_id(self):
"""Return a unique ID."""
return self._unique_id

@property
def name(self):
"""Return the name of the sensor."""
return "{} {} ({})".format(self._base_name,SENSOR_TYPES[self._type].get('name'),self._player.player_id)
return "{} {} ({})".format(self._base_name, SENSOR_TYPES[self._type].get('name'), self._player.player_id)

@property
def friendly_name(self):
"""Return the friendly name of the sensor."""
return "{} {} ({})".format(self._base_name,SENSOR_TYPES[self._type].get('name'),self._player.friendly_player_id)
return "{} {} ({})".format(self._base_name, SENSOR_TYPES[self._type].get('name'), self._player.friendly_player_id)

@property
def device_state_attributes(self):
def extra_state_attributes(self):
"""Return the state attributes."""
return self._attrs

Expand All @@ -200,18 +210,21 @@ def icon(self):

def update(self):
"""Get the latest data from the sensor."""
self._state = getattr(self._player,self._type)
self._state = getattr(self._player, self._type)
if self._type == 'online':
p = self._player.player_profile
self._attrs.update({k: p[k] for k in p if k not in ZWIFT_IGNORED_PROFILE_ATTRIBUTES})
self._attrs.update(
{k: p[k] for k in p if k not in ZWIFT_IGNORED_PROFILE_ATTRIBUTES})

async def async_added_to_hass(self):
"""Register update signal handler."""
async def async_update_state():
"""Update sensor state."""
await self.async_update_ha_state(True)

async_dispatcher_connect(self.hass, SIGNAL_ZWIFT_UPDATE.format(player_id=self._player.player_id), async_update_state)
async_dispatcher_connect(self.hass, SIGNAL_ZWIFT_UPDATE.format(
player_id=self._player.player_id), async_update_state)


class ZwiftBinarySensorDevice(ZwiftSensorDevice, BinarySensorEntity):
@property
Expand All @@ -224,6 +237,7 @@ def device_class(self):
"""Return the device class of the binary sensor."""
return SENSOR_TYPES[self._type].get('device_class')


class ZwiftPlayerData:
def __init__(self, player_id):
self._player_id = player_id
Expand All @@ -240,7 +254,7 @@ def friendly_player_id(self):

@property
def online(self):
return self.data.get('online',False)
return self.data.get('online', False)

@property
def hr(self):
Expand Down Expand Up @@ -272,22 +286,24 @@ def gradient(self):

@property
def level(self):
return self.player_profile.get('playerLevel',None)
return self.player_profile.get('playerLevel', None)

@property
def runlevel(self):
return self.player_profile.get('runLevel',None)
return self.player_profile.get('runLevel', None)

@property
def cycleprogress(self):
return self.player_profile.get('cycleProgress',None)
return self.player_profile.get('cycleProgress', None)

@property
def runprogress(self):
return self.player_profile.get('runProgress',None)
return self.player_profile.get('runProgress', None)


class ZwiftData:
"""Representation of a Zwift client data collection object."""

def __init__(self, update_interval, username, password, players, hass):
self._client = None
self.username = username
Expand Down Expand Up @@ -318,12 +334,12 @@ async def check_zwift_auth(self, client):
@property
def is_metric(self):
if self._profile:
return self._profile.get('useMetric',False)
return self._profile.get('useMetric', False)
return False

async def _connect(self):
from zwift import Client as ZwiftClient
client = ZwiftClient(self.username,self.password)
client = ZwiftClient(self.username, self.password)
if await self.check_zwift_auth(client):
self._client = client
self._profile = await self.hass.async_add_executor_job(self._get_self_profile)
Expand All @@ -343,35 +359,48 @@ def update(self):

_profile = self._client.get_profile(player_id)
player_profile = _profile.profile or {}
_LOGGER.debug("Zwift profile data: {}".format(player_profile))
total_experience = int(player_profile.get('totalExperiencePoints'))
player_profile['playerLevel'] = int(player_profile.get('achievementLevel',0) / 100)
player_profile['runLevel'] = int(player_profile.get('runAchievementLevel',0) / 100)
player_profile['cycleProgress'] = int(player_profile.get('achievementLevel',0) % 100)
player_profile['runProgress'] = int(player_profile.get('runAchievementLevel',0) % 100)
_LOGGER.debug(
"Zwift profile data: {}".format(player_profile))
total_experience = int(
player_profile.get('totalExperiencePoints'))
player_profile['playerLevel'] = int(
player_profile.get('achievementLevel', 0) / 100)
player_profile['runLevel'] = int(
player_profile.get('runAchievementLevel', 0) / 100)
player_profile['cycleProgress'] = int(
player_profile.get('achievementLevel', 0) % 100)
player_profile['runProgress'] = int(
player_profile.get('runAchievementLevel', 0) % 100)
latest_activity = _profile.latest_activity
latest_activity['world_name'] = ZWIFT_WORLDS.get(latest_activity.get('worldId'))
latest_activity['world_name'] = ZWIFT_WORLDS.get(
latest_activity.get('worldId'))
player_profile['latest_activity'] = latest_activity

data['total_experience'] = total_experience
data['level'] = player_profile['playerLevel']
player_profile['world_name'] = ZWIFT_WORLDS.get(player_profile.get('worldId'))
player_profile['world_name'] = ZWIFT_WORLDS.get(
player_profile.get('worldId'))

if player_profile.get('riding'):
player_state = world.player_status(player_id)
_LOGGER.debug("Zwift player state data: {}".format(player_state.player_state))
altitude = (float(player_state.altitude) - 9000) / 2 # [TODO] is this correct regardless of metric/imperial? Correct regardless of world?
_LOGGER.debug("Zwift player state data: {}".format(
player_state.player_state))
# [TODO] is this correct regardless of metric/imperial? Correct regardless of world?
altitude = (float(player_state.altitude) - 9000) / 2
distance = float(player_state.distance)
gradient = self.players[player_id].data.get('gradient', 0)
rideons = latest_activity.get('activityRideOnCount',0)
if rideons > 0 and rideons > self.players[player_id].data.get('rideons',0):
gradient = self.players[player_id].data.get(
'gradient', 0)
rideons = latest_activity.get('activityRideOnCount', 0)
if rideons > 0 and rideons > self.players[player_id].data.get('rideons', 0):
self.hass.bus.fire(EVENT_ZWIFT_RIDE_ON, {
'player_id': player_id,
'rideons': rideons
})
if self.players[player_id].data.get('distance',0) > 0:
delta_distance = distance - self.players[player_id].data.get('distance',0)
delta_altitude = altitude - self.players[player_id].data.get('altitude',0)
if self.players[player_id].data.get('distance', 0) > 0:
delta_distance = distance - \
self.players[player_id].data.get('distance', 0)
delta_altitude = altitude - \
self.players[player_id].data.get('altitude', 0)
if delta_distance > 0:
gradient = delta_altitude / delta_distance
data.update({
Expand All @@ -391,18 +420,24 @@ def update(self):
except RequestException as e:
if '401' in str(e):
self._client = None
_LOGGER.warning('Zwift credentials are wrong or expired')
_LOGGER.warning(
'Zwift credentials are wrong or expired')
elif '404' in str(e):
_LOGGER.warning('Upstream Zwift 404 - will try later')
elif '429' in str(e):
current_interval = self.online_update_interval
new_interval = self.online_update_interval + timedelta(seconds=0.25)
new_interval = self.online_update_interval + \
timedelta(seconds=0.25)
self.online_update_interval = new_interval
_LOGGER.warning('Upstream request throttling 429 - known issue, increasing interval from {}s to {}s'.format(current_interval.total_seconds(),new_interval.total_seconds()))
_LOGGER.warning('Upstream request throttling 429 - known issue, increasing interval from {}s to {}s'.format(
current_interval.total_seconds(), new_interval.total_seconds()))
else:
_LOGGER.exception('something went wrong in Zwift python library - {} while updating zwift sensor for player {}'.format(str(e), player_id))
_LOGGER.exception(
'something went wrong in Zwift python library - {} while updating zwift sensor for player {}'.format(str(e), player_id))
except Exception as e:
_LOGGER.exception('something went major wrong while updating zwift sensor for player {}'.format(player_id))
_LOGGER.debug("dispatching zwift data update for player {}".format(player_id))
dispatcher_send(self.hass, SIGNAL_ZWIFT_UPDATE.format(player_id=player_id))

_LOGGER.exception(
'something went major wrong while updating zwift sensor for player {}'.format(player_id))
_LOGGER.debug(
"dispatching zwift data update for player {}".format(player_id))
dispatcher_send(
self.hass, SIGNAL_ZWIFT_UPDATE.format(player_id=player_id))
7 changes: 4 additions & 3 deletions hacs.json
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
{
"name": "Zwift Sensors",
"domains": ["sensor"]
}
"name": "Zwift Sensors",
"domains": ["sensor"],
"homeassistant": "2021.12"
}

0 comments on commit c61a623

Please sign in to comment.