core/homeassistant/components/steam_online/sensor.py

231 lines
7.1 KiB
Python
Raw Normal View History

"""Sensor for Steam account status."""
from __future__ import annotations
2019-07-10 18:15:42 +00:00
from datetime import timedelta
import logging
from time import mktime
2017-03-02 14:58:35 +00:00
import steam
2016-09-04 02:21:59 +00:00
import voluptuous as vol
from homeassistant.components.sensor import PLATFORM_SCHEMA, SensorEntity
from homeassistant.const import CONF_API_KEY
from homeassistant.core import HomeAssistant, callback
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.entity_platform import AddEntitiesCallback
from homeassistant.helpers.event import track_time_interval
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
from homeassistant.util.dt import utc_from_timestamp
2016-09-04 02:21:59 +00:00
2017-03-02 14:58:35 +00:00
_LOGGER = logging.getLogger(__name__)
2019-07-31 19:25:30 +00:00
CONF_ACCOUNTS = "accounts"
ICON = "mdi:steam"
STATE_OFFLINE = "offline"
STATE_ONLINE = "online"
STATE_BUSY = "busy"
STATE_AWAY = "away"
STATE_SNOOZE = "snooze"
STATE_LOOKING_TO_TRADE = "looking_to_trade"
STATE_LOOKING_TO_PLAY = "looking_to_play"
STEAM_API_URL = "https://steamcdn-a.akamaihd.net/steam/apps/"
STEAM_HEADER_IMAGE_FILE = "header.jpg"
STEAM_MAIN_IMAGE_FILE = "capsule_616x353.jpg"
STEAM_ICON_URL = (
"https://steamcdn-a.akamaihd.net/steamcommunity/public/images/apps/%d/%s.jpg"
)
2019-07-31 19:25:30 +00:00
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend(
{
vol.Required(CONF_API_KEY): cv.string,
vol.Required(CONF_ACCOUNTS, default=[]): vol.All(cv.ensure_list, [cv.string]),
}
)
APP_LIST_KEY = "steam_online.app_list"
2019-07-10 18:15:42 +00:00
BASE_INTERVAL = timedelta(minutes=1)
2016-02-20 23:08:18 +00:00
def setup_platform(
hass: HomeAssistant,
config: ConfigType,
add_entities: AddEntitiesCallback,
discovery_info: DiscoveryInfoType | None = None,
) -> None:
"""Set up the Steam platform."""
2019-07-31 19:25:30 +00:00
steam.api.key.set(config[CONF_API_KEY])
Proper Steam game names and small fixes (#11182) * Use constant for offline state * Use constant for no game name * Rename trade and play constant their proper names Trade and Play are not the correct names for the states. For instance Play might be seens as the user is actually is playing, which is not correct as there is no such state is returned from the Steam API. Just having "trade" does not say much about what is happening and might be misintepreted that the user is currently trading, which is not correct either. We instead use the names from the underlying library for naming the states [1] [1] https://github.com/Lagg/steamodd/blob/2e518ad84f3afce631d5d7eca3af0f85b5330b5b/steam/user.py#L109 * Get the proper game name if no extra info is given from the api The third `current_game` parameter that was used before hold extra information about the game that is being played. This might contain the game name, it might also be empty. The correct way to get the game name is to fetch it from the API depending on the game id that might be returned in the `current_game` attribute if the user is playing a game. To not break existing implementations we keep the functionality to first go with the extra information and only then fetch the proper game name. * Refactor getting game name to its own function This cleans up the code and removed "ugly" else statements from the sensor and makes the game fetching easier to read. * Let state constant values be lower snake case * Return None instead of 'None' when no current game exists * Initialize steam app list only once to benefit form caching * Return None as state attributes if no current game is present
2017-12-21 03:32:33 +00:00
# Initialize steammods app list before creating sensors
# to benefit from internal caching of the list.
hass.data[APP_LIST_KEY] = steam.apps.app_list()
entities = [SteamSensor(account, steam) for account in config[CONF_ACCOUNTS]]
2019-07-10 18:15:42 +00:00
if not entities:
return
add_entities(entities, True)
# Only one sensor update once every 60 seconds to avoid
# flooding steam and getting disconnected.
entity_next = 0
2019-07-31 19:25:30 +00:00
2019-07-10 18:15:42 +00:00
@callback
def do_update(time):
nonlocal entity_next
entities[entity_next].async_schedule_update_ha_state(True)
entity_next = (entity_next + 1) % len(entities)
track_time_interval(hass, do_update, BASE_INTERVAL)
2016-02-20 23:08:18 +00:00
class SteamSensor(SensorEntity):
2016-02-24 09:47:35 +00:00
"""A class for the Steam account."""
2016-03-08 15:46:34 +00:00
2019-07-10 18:15:42 +00:00
def __init__(self, account, steamod):
2016-03-08 15:46:34 +00:00
"""Initialize the sensor."""
2016-02-20 23:08:18 +00:00
self._steamod = steamod
self._account = account
self._profile = None
self._game = None
self._game_id = None
self._extra_game_info = None
self._state = None
self._name = None
self._avatar = None
self._last_online = None
self._level = None
self._owned_games = None
2016-02-20 23:08:18 +00:00
@property
def name(self):
2016-02-24 09:47:35 +00:00
"""Return the name of the sensor."""
2017-03-02 14:58:35 +00:00
return self._name
2016-02-20 23:08:18 +00:00
@property
def entity_id(self):
2016-02-24 09:47:35 +00:00
"""Return the entity ID."""
return f"sensor.steam_{self._account}"
2016-02-20 23:08:18 +00:00
@property
def native_value(self):
2016-02-24 09:47:35 +00:00
"""Return the state of the sensor."""
2016-02-23 18:01:50 +00:00
return self._state
2016-02-20 23:08:18 +00:00
2019-07-10 18:15:42 +00:00
@property
def should_poll(self):
"""Turn off polling, will do ourselves."""
return False
2016-02-20 23:08:18 +00:00
def update(self):
2016-02-24 09:47:35 +00:00
"""Update device state."""
2017-03-02 14:58:35 +00:00
try:
self._profile = self._steamod.user.profile(self._account)
# Only if need be, get the owned games
if not self._owned_games:
self._owned_games = self._steamod.api.interface(
"IPlayerService"
).GetOwnedGames(steamid=self._account, include_appinfo=1)
Proper Steam game names and small fixes (#11182) * Use constant for offline state * Use constant for no game name * Rename trade and play constant their proper names Trade and Play are not the correct names for the states. For instance Play might be seens as the user is actually is playing, which is not correct as there is no such state is returned from the Steam API. Just having "trade" does not say much about what is happening and might be misintepreted that the user is currently trading, which is not correct either. We instead use the names from the underlying library for naming the states [1] [1] https://github.com/Lagg/steamodd/blob/2e518ad84f3afce631d5d7eca3af0f85b5330b5b/steam/user.py#L109 * Get the proper game name if no extra info is given from the api The third `current_game` parameter that was used before hold extra information about the game that is being played. This might contain the game name, it might also be empty. The correct way to get the game name is to fetch it from the API depending on the game id that might be returned in the `current_game` attribute if the user is playing a game. To not break existing implementations we keep the functionality to first go with the extra information and only then fetch the proper game name. * Refactor getting game name to its own function This cleans up the code and removed "ugly" else statements from the sensor and makes the game fetching easier to read. * Let state constant values be lower snake case * Return None instead of 'None' when no current game exists * Initialize steam app list only once to benefit form caching * Return None as state attributes if no current game is present
2017-12-21 03:32:33 +00:00
self._game = self._get_current_game()
self._game_id = self._profile.current_game[0]
self._extra_game_info = self._get_game_info()
2017-03-02 14:58:35 +00:00
self._state = {
2017-03-10 03:55:18 +00:00
1: STATE_ONLINE,
2: STATE_BUSY,
3: STATE_AWAY,
4: STATE_SNOOZE,
Proper Steam game names and small fixes (#11182) * Use constant for offline state * Use constant for no game name * Rename trade and play constant their proper names Trade and Play are not the correct names for the states. For instance Play might be seens as the user is actually is playing, which is not correct as there is no such state is returned from the Steam API. Just having "trade" does not say much about what is happening and might be misintepreted that the user is currently trading, which is not correct either. We instead use the names from the underlying library for naming the states [1] [1] https://github.com/Lagg/steamodd/blob/2e518ad84f3afce631d5d7eca3af0f85b5330b5b/steam/user.py#L109 * Get the proper game name if no extra info is given from the api The third `current_game` parameter that was used before hold extra information about the game that is being played. This might contain the game name, it might also be empty. The correct way to get the game name is to fetch it from the API depending on the game id that might be returned in the `current_game` attribute if the user is playing a game. To not break existing implementations we keep the functionality to first go with the extra information and only then fetch the proper game name. * Refactor getting game name to its own function This cleans up the code and removed "ugly" else statements from the sensor and makes the game fetching easier to read. * Let state constant values be lower snake case * Return None instead of 'None' when no current game exists * Initialize steam app list only once to benefit form caching * Return None as state attributes if no current game is present
2017-12-21 03:32:33 +00:00
5: STATE_LOOKING_TO_TRADE,
6: STATE_LOOKING_TO_PLAY,
}.get(self._profile.status, STATE_OFFLINE)
2017-03-02 14:58:35 +00:00
self._name = self._profile.persona
self._avatar = self._profile.avatar_medium
self._last_online = self._get_last_online()
self._level = self._profile.level
2017-03-02 14:58:35 +00:00
except self._steamod.api.HTTPTimeoutError as error:
_LOGGER.warning(error)
self._game = None
self._game_id = None
self._state = None
self._name = None
self._avatar = None
self._last_online = None
self._level = None
2016-02-20 23:08:18 +00:00
Proper Steam game names and small fixes (#11182) * Use constant for offline state * Use constant for no game name * Rename trade and play constant their proper names Trade and Play are not the correct names for the states. For instance Play might be seens as the user is actually is playing, which is not correct as there is no such state is returned from the Steam API. Just having "trade" does not say much about what is happening and might be misintepreted that the user is currently trading, which is not correct either. We instead use the names from the underlying library for naming the states [1] [1] https://github.com/Lagg/steamodd/blob/2e518ad84f3afce631d5d7eca3af0f85b5330b5b/steam/user.py#L109 * Get the proper game name if no extra info is given from the api The third `current_game` parameter that was used before hold extra information about the game that is being played. This might contain the game name, it might also be empty. The correct way to get the game name is to fetch it from the API depending on the game id that might be returned in the `current_game` attribute if the user is playing a game. To not break existing implementations we keep the functionality to first go with the extra information and only then fetch the proper game name. * Refactor getting game name to its own function This cleans up the code and removed "ugly" else statements from the sensor and makes the game fetching easier to read. * Let state constant values be lower snake case * Return None instead of 'None' when no current game exists * Initialize steam app list only once to benefit form caching * Return None as state attributes if no current game is present
2017-12-21 03:32:33 +00:00
def _get_current_game(self):
"""Gather current game name from APP ID."""
2021-10-19 02:36:35 +00:00
if game_extra_info := self._profile.current_game[2]:
Proper Steam game names and small fixes (#11182) * Use constant for offline state * Use constant for no game name * Rename trade and play constant their proper names Trade and Play are not the correct names for the states. For instance Play might be seens as the user is actually is playing, which is not correct as there is no such state is returned from the Steam API. Just having "trade" does not say much about what is happening and might be misintepreted that the user is currently trading, which is not correct either. We instead use the names from the underlying library for naming the states [1] [1] https://github.com/Lagg/steamodd/blob/2e518ad84f3afce631d5d7eca3af0f85b5330b5b/steam/user.py#L109 * Get the proper game name if no extra info is given from the api The third `current_game` parameter that was used before hold extra information about the game that is being played. This might contain the game name, it might also be empty. The correct way to get the game name is to fetch it from the API depending on the game id that might be returned in the `current_game` attribute if the user is playing a game. To not break existing implementations we keep the functionality to first go with the extra information and only then fetch the proper game name. * Refactor getting game name to its own function This cleans up the code and removed "ugly" else statements from the sensor and makes the game fetching easier to read. * Let state constant values be lower snake case * Return None instead of 'None' when no current game exists * Initialize steam app list only once to benefit form caching * Return None as state attributes if no current game is present
2017-12-21 03:32:33 +00:00
return game_extra_info
2021-10-19 02:36:35 +00:00
if not (game_id := self._profile.current_game[0]):
2019-07-10 18:15:42 +00:00
return None
app_list = self.hass.data[APP_LIST_KEY]
try:
_, res = app_list[game_id]
return res
except KeyError:
pass
# Try reloading the app list, must be a new app
app_list = self._steamod.apps.app_list()
self.hass.data[APP_LIST_KEY] = app_list
try:
_, res = app_list[game_id]
return res
except KeyError:
pass
Proper Steam game names and small fixes (#11182) * Use constant for offline state * Use constant for no game name * Rename trade and play constant their proper names Trade and Play are not the correct names for the states. For instance Play might be seens as the user is actually is playing, which is not correct as there is no such state is returned from the Steam API. Just having "trade" does not say much about what is happening and might be misintepreted that the user is currently trading, which is not correct either. We instead use the names from the underlying library for naming the states [1] [1] https://github.com/Lagg/steamodd/blob/2e518ad84f3afce631d5d7eca3af0f85b5330b5b/steam/user.py#L109 * Get the proper game name if no extra info is given from the api The third `current_game` parameter that was used before hold extra information about the game that is being played. This might contain the game name, it might also be empty. The correct way to get the game name is to fetch it from the API depending on the game id that might be returned in the `current_game` attribute if the user is playing a game. To not break existing implementations we keep the functionality to first go with the extra information and only then fetch the proper game name. * Refactor getting game name to its own function This cleans up the code and removed "ugly" else statements from the sensor and makes the game fetching easier to read. * Let state constant values be lower snake case * Return None instead of 'None' when no current game exists * Initialize steam app list only once to benefit form caching * Return None as state attributes if no current game is present
2017-12-21 03:32:33 +00:00
2019-07-10 18:15:42 +00:00
_LOGGER.error("Unable to find name of app with ID=%s", game_id)
return repr(game_id)
Proper Steam game names and small fixes (#11182) * Use constant for offline state * Use constant for no game name * Rename trade and play constant their proper names Trade and Play are not the correct names for the states. For instance Play might be seens as the user is actually is playing, which is not correct as there is no such state is returned from the Steam API. Just having "trade" does not say much about what is happening and might be misintepreted that the user is currently trading, which is not correct either. We instead use the names from the underlying library for naming the states [1] [1] https://github.com/Lagg/steamodd/blob/2e518ad84f3afce631d5d7eca3af0f85b5330b5b/steam/user.py#L109 * Get the proper game name if no extra info is given from the api The third `current_game` parameter that was used before hold extra information about the game that is being played. This might contain the game name, it might also be empty. The correct way to get the game name is to fetch it from the API depending on the game id that might be returned in the `current_game` attribute if the user is playing a game. To not break existing implementations we keep the functionality to first go with the extra information and only then fetch the proper game name. * Refactor getting game name to its own function This cleans up the code and removed "ugly" else statements from the sensor and makes the game fetching easier to read. * Let state constant values be lower snake case * Return None instead of 'None' when no current game exists * Initialize steam app list only once to benefit form caching * Return None as state attributes if no current game is present
2017-12-21 03:32:33 +00:00
def _get_game_info(self):
2021-10-19 02:36:35 +00:00
if (game_id := self._profile.current_game[0]) is not None:
for game in self._owned_games["response"]["games"]:
if game["appid"] == game_id:
return game
return None
def _get_last_online(self):
"""Convert last_online from the steam module into timestamp UTC."""
last_online = utc_from_timestamp(mktime(self._profile.last_online))
if last_online:
return last_online
return None
@property
def extra_state_attributes(self):
2016-03-08 15:46:34 +00:00
"""Return the state attributes."""
attr = {}
if self._game is not None:
attr["game"] = self._game
if self._game_id is not None:
attr["game_id"] = self._game_id
game_url = f"{STEAM_API_URL}{self._game_id}/"
attr["game_image_header"] = f"{game_url}{STEAM_HEADER_IMAGE_FILE}"
attr["game_image_main"] = f"{game_url}{STEAM_MAIN_IMAGE_FILE}"
if self._extra_game_info is not None and self._game_id is not None:
attr["game_icon"] = STEAM_ICON_URL % (
self._game_id,
self._extra_game_info["img_icon_url"],
)
if self._last_online is not None:
attr["last_online"] = self._last_online
if self._level is not None:
attr["level"] = self._level
return attr
2016-02-20 23:08:18 +00:00
@property
2016-02-24 06:41:24 +00:00
def entity_picture(self):
"""Avatar of the account."""
2017-03-02 14:58:35 +00:00
return self._avatar
2016-02-20 23:08:18 +00:00
@property
def icon(self):
2016-02-24 09:47:35 +00:00
"""Return the icon to use in the frontend."""
2016-02-20 23:08:18 +00:00
return ICON