mirror of
https://github.com/home-assistant/core.git
synced 2025-04-25 01:38:02 +00:00
Add Epic Games Store integration (#104725)
* Add Epic Games Store integration
Squashed commit of the following PR: #81167
* Bump epicstore-api to 0.1.7 as it handle better error 1004
Thanks to d7469f7c99
* Use extra_state_attributes instead of overriding state_attributes
* Review: change how config_flow.validate_input is handled
* Use LanguageSelector and rename locale to language
* Review: init-better use of hass.data.setdefault
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
* Review: don't need to update at init
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
* Revert "Review: don't need to update at init" --> not working otherwise
This reverts commit 1445a87c8e9b7247f1c9835bf2e2d7297dd02586.
* Review: fix config_flow.validate_input/retactor following lib bump
* review: merge async_update function with event property
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
* hassfest
* Fix duplicates data from applied comment review 5035055
* review: thanks to 5035055 async_add_entities update_before_add param is not required anymore
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
* Fix Christmas special "Holiday sale" case
* gen_requirements_all
* Use CONF_LANGUAGE from HA const
* Move CalendarType to const
* manifest: integration_type -> service
Co-authored-by: Sid <27780930+autinerd@users.noreply.github.com>
* calendar: remove date start/end assert
Co-authored-by: Erik Montnemery <erik@montnemery.com>
* const: rename SUPPORTED_LANGUAGES
* hassfest
* config: Move to ConfigFlowResult
* coordinator: main file comment
Co-authored-by: Erik Montnemery <erik@montnemery.com>
* ruff & hassfest
* review: do not guess country
* Add @hacf-fr as codeowner
* review: remove games extra_attrs
Was dropped somehow:
- 73c20f34803b0a0ec242bf0740494f17a68f6f59 review: move games extra_attrs to data service
- other commit that removed the service part
* review: remove unused error class
was removed:
- 040cf945bb5346b6d42b3782b5061a13fb7b1f6b
---------
Co-authored-by: Joost Lekkerkerker <joostlek@outlook.com>
Co-authored-by: Sid <27780930+autinerd@users.noreply.github.com>
Co-authored-by: Erik Montnemery <erik@montnemery.com>
This commit is contained in:
parent
66ea528e94
commit
f927b27ed4
@ -361,6 +361,8 @@ omit =
|
||||
homeassistant/components/environment_canada/weather.py
|
||||
homeassistant/components/envisalink/*
|
||||
homeassistant/components/ephember/climate.py
|
||||
homeassistant/components/epic_games_store/__init__.py
|
||||
homeassistant/components/epic_games_store/coordinator.py
|
||||
homeassistant/components/epion/__init__.py
|
||||
homeassistant/components/epion/coordinator.py
|
||||
homeassistant/components/epion/sensor.py
|
||||
|
@ -398,6 +398,8 @@ build.json @home-assistant/supervisor
|
||||
/homeassistant/components/environment_canada/ @gwww @michaeldavie
|
||||
/tests/components/environment_canada/ @gwww @michaeldavie
|
||||
/homeassistant/components/ephember/ @ttroy50
|
||||
/homeassistant/components/epic_games_store/ @hacf-fr @Quentame
|
||||
/tests/components/epic_games_store/ @hacf-fr @Quentame
|
||||
/homeassistant/components/epion/ @lhgravendeel
|
||||
/tests/components/epion/ @lhgravendeel
|
||||
/homeassistant/components/epson/ @pszafer
|
||||
|
35
homeassistant/components/epic_games_store/__init__.py
Normal file
35
homeassistant/components/epic_games_store/__init__.py
Normal file
@ -0,0 +1,35 @@
|
||||
"""The Epic Games Store integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
from .const import DOMAIN
|
||||
from .coordinator import EGSCalendarUpdateCoordinator
|
||||
|
||||
PLATFORMS: list[Platform] = [
|
||||
Platform.CALENDAR,
|
||||
]
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Set up Epic Games Store from a config entry."""
|
||||
|
||||
coordinator = EGSCalendarUpdateCoordinator(hass, entry)
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
|
||||
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = coordinator
|
||||
|
||||
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"""Unload a config entry."""
|
||||
if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS):
|
||||
hass.data[DOMAIN].pop(entry.entry_id)
|
||||
|
||||
return unload_ok
|
97
homeassistant/components/epic_games_store/calendar.py
Normal file
97
homeassistant/components/epic_games_store/calendar.py
Normal file
@ -0,0 +1,97 @@
|
||||
"""Calendar platform for a Epic Games Store."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from collections import namedtuple
|
||||
from datetime import datetime
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.components.calendar import CalendarEntity, CalendarEvent
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.device_registry import DeviceEntryType, DeviceInfo
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.update_coordinator import CoordinatorEntity
|
||||
|
||||
from .const import DOMAIN, CalendarType
|
||||
from .coordinator import EGSCalendarUpdateCoordinator
|
||||
|
||||
DateRange = namedtuple("DateRange", ["start", "end"])
|
||||
|
||||
|
||||
async def async_setup_entry(
|
||||
hass: HomeAssistant,
|
||||
entry: ConfigEntry,
|
||||
async_add_entities: AddEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up the local calendar platform."""
|
||||
coordinator: EGSCalendarUpdateCoordinator = hass.data[DOMAIN][entry.entry_id]
|
||||
|
||||
entities = [
|
||||
EGSCalendar(coordinator, entry.entry_id, CalendarType.FREE),
|
||||
EGSCalendar(coordinator, entry.entry_id, CalendarType.DISCOUNT),
|
||||
]
|
||||
async_add_entities(entities)
|
||||
|
||||
|
||||
class EGSCalendar(CoordinatorEntity[EGSCalendarUpdateCoordinator], CalendarEntity):
|
||||
"""A calendar entity by Epic Games Store."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
coordinator: EGSCalendarUpdateCoordinator,
|
||||
config_entry_id: str,
|
||||
cal_type: CalendarType,
|
||||
) -> None:
|
||||
"""Initialize EGSCalendar."""
|
||||
super().__init__(coordinator)
|
||||
self._cal_type = cal_type
|
||||
self._attr_translation_key = f"{cal_type}_games"
|
||||
self._attr_unique_id = f"{config_entry_id}-{cal_type}"
|
||||
self._attr_device_info = DeviceInfo(
|
||||
entry_type=DeviceEntryType.SERVICE,
|
||||
identifiers={(DOMAIN, config_entry_id)},
|
||||
manufacturer="Epic Games Store",
|
||||
name="Epic Games Store",
|
||||
)
|
||||
|
||||
@property
|
||||
def event(self) -> CalendarEvent | None:
|
||||
"""Return the next upcoming event."""
|
||||
if event := self.coordinator.data[self._cal_type]:
|
||||
return _get_calendar_event(event[0])
|
||||
return None
|
||||
|
||||
async def async_get_events(
|
||||
self, hass: HomeAssistant, start_date: datetime, end_date: datetime
|
||||
) -> list[CalendarEvent]:
|
||||
"""Get all events in a specific time frame."""
|
||||
events = filter(
|
||||
lambda game: _are_date_range_overlapping(
|
||||
DateRange(start=game["discount_start_at"], end=game["discount_end_at"]),
|
||||
DateRange(start=start_date, end=end_date),
|
||||
),
|
||||
self.coordinator.data[self._cal_type],
|
||||
)
|
||||
return [_get_calendar_event(event) for event in events]
|
||||
|
||||
|
||||
def _get_calendar_event(event: dict[str, Any]) -> CalendarEvent:
|
||||
"""Return a CalendarEvent from an API event."""
|
||||
return CalendarEvent(
|
||||
summary=event["title"],
|
||||
start=event["discount_start_at"],
|
||||
end=event["discount_end_at"],
|
||||
description=f"{event['description']}\n\n{event['url']}",
|
||||
)
|
||||
|
||||
|
||||
def _are_date_range_overlapping(range1: DateRange, range2: DateRange) -> bool:
|
||||
"""Return a CalendarEvent from an API event."""
|
||||
latest_start = max(range1.start, range2.start)
|
||||
earliest_end = min(range1.end, range2.end)
|
||||
delta = (earliest_end - latest_start).days + 1
|
||||
overlap = max(0, delta)
|
||||
return overlap > 0
|
96
homeassistant/components/epic_games_store/config_flow.py
Normal file
96
homeassistant/components/epic_games_store/config_flow.py
Normal file
@ -0,0 +1,96 @@
|
||||
"""Config flow for Epic Games Store integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from epicstore_api import EpicGamesStoreAPI
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.config_entries import ConfigFlowResult
|
||||
from homeassistant.const import CONF_COUNTRY, CONF_LANGUAGE
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.selector import (
|
||||
CountrySelector,
|
||||
LanguageSelector,
|
||||
LanguageSelectorConfig,
|
||||
)
|
||||
|
||||
from .const import DOMAIN, SUPPORTED_LANGUAGES
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
STEP_USER_DATA_SCHEMA = vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_LANGUAGE): LanguageSelector(
|
||||
LanguageSelectorConfig(languages=SUPPORTED_LANGUAGES)
|
||||
),
|
||||
vol.Required(CONF_COUNTRY): CountrySelector(),
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
def get_default_language(hass: HomeAssistant) -> str | None:
|
||||
"""Get default language code based on Home Assistant config."""
|
||||
language_code = f"{hass.config.language}-{hass.config.country}"
|
||||
if language_code in SUPPORTED_LANGUAGES:
|
||||
return language_code
|
||||
if hass.config.language in SUPPORTED_LANGUAGES:
|
||||
return hass.config.language
|
||||
return None
|
||||
|
||||
|
||||
async def validate_input(hass: HomeAssistant, user_input: dict[str, Any]) -> None:
|
||||
"""Validate the user input allows us to connect."""
|
||||
api = EpicGamesStoreAPI(user_input[CONF_LANGUAGE], user_input[CONF_COUNTRY])
|
||||
data = await hass.async_add_executor_job(api.get_free_games)
|
||||
|
||||
if data.get("errors"):
|
||||
_LOGGER.warning(data["errors"])
|
||||
|
||||
assert data["data"]["Catalog"]["searchStore"]["elements"]
|
||||
|
||||
|
||||
class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for Epic Games Store."""
|
||||
|
||||
VERSION = 1
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle the initial step."""
|
||||
data_schema = self.add_suggested_values_to_schema(
|
||||
STEP_USER_DATA_SCHEMA,
|
||||
user_input
|
||||
or {
|
||||
CONF_LANGUAGE: get_default_language(self.hass),
|
||||
CONF_COUNTRY: self.hass.config.country,
|
||||
},
|
||||
)
|
||||
if user_input is None:
|
||||
return self.async_show_form(step_id="user", data_schema=data_schema)
|
||||
|
||||
await self.async_set_unique_id(
|
||||
f"freegames-{user_input[CONF_LANGUAGE]}-{user_input[CONF_COUNTRY]}"
|
||||
)
|
||||
self._abort_if_unique_id_configured()
|
||||
|
||||
errors = {}
|
||||
|
||||
try:
|
||||
await validate_input(self.hass, user_input)
|
||||
except Exception: # pylint: disable=broad-except
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
errors["base"] = "unknown"
|
||||
else:
|
||||
return self.async_create_entry(
|
||||
title=f"Epic Games Store - Free Games ({user_input[CONF_LANGUAGE]}-{user_input[CONF_COUNTRY]})",
|
||||
data=user_input,
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="user", data_schema=data_schema, errors=errors
|
||||
)
|
31
homeassistant/components/epic_games_store/const.py
Normal file
31
homeassistant/components/epic_games_store/const.py
Normal file
@ -0,0 +1,31 @@
|
||||
"""Constants for the Epic Games Store integration."""
|
||||
|
||||
from enum import StrEnum
|
||||
|
||||
DOMAIN = "epic_games_store"
|
||||
|
||||
SUPPORTED_LANGUAGES = [
|
||||
"ar",
|
||||
"de",
|
||||
"en-US",
|
||||
"es-ES",
|
||||
"es-MX",
|
||||
"fr",
|
||||
"it",
|
||||
"ja",
|
||||
"ko",
|
||||
"pl",
|
||||
"pt-BR",
|
||||
"ru",
|
||||
"th",
|
||||
"tr",
|
||||
"zh-CN",
|
||||
"zh-Hant",
|
||||
]
|
||||
|
||||
|
||||
class CalendarType(StrEnum):
|
||||
"""Calendar types."""
|
||||
|
||||
FREE = "free"
|
||||
DISCOUNT = "discount"
|
81
homeassistant/components/epic_games_store/coordinator.py
Normal file
81
homeassistant/components/epic_games_store/coordinator.py
Normal file
@ -0,0 +1,81 @@
|
||||
"""The Epic Games Store integration data coordinator."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import timedelta
|
||||
import logging
|
||||
from typing import Any
|
||||
|
||||
from epicstore_api import EpicGamesStoreAPI
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_COUNTRY, CONF_LANGUAGE
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator
|
||||
|
||||
from .const import DOMAIN, CalendarType
|
||||
from .helper import format_game_data
|
||||
|
||||
SCAN_INTERVAL = timedelta(days=1)
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class EGSCalendarUpdateCoordinator(
|
||||
DataUpdateCoordinator[dict[str, list[dict[str, Any]]]]
|
||||
):
|
||||
"""Class to manage fetching data from the Epic Game Store."""
|
||||
|
||||
def __init__(self, hass: HomeAssistant, entry: ConfigEntry) -> None:
|
||||
"""Initialize."""
|
||||
self._api = EpicGamesStoreAPI(
|
||||
entry.data[CONF_LANGUAGE],
|
||||
entry.data[CONF_COUNTRY],
|
||||
)
|
||||
self.language = entry.data[CONF_LANGUAGE]
|
||||
|
||||
super().__init__(
|
||||
hass,
|
||||
_LOGGER,
|
||||
name=DOMAIN,
|
||||
update_interval=SCAN_INTERVAL,
|
||||
)
|
||||
|
||||
async def _async_update_data(self) -> dict[str, list[dict[str, Any]]]:
|
||||
"""Update data via library."""
|
||||
raw_data = await self.hass.async_add_executor_job(self._api.get_free_games)
|
||||
_LOGGER.debug(raw_data)
|
||||
data = raw_data["data"]["Catalog"]["searchStore"]["elements"]
|
||||
|
||||
discount_games = filter(
|
||||
lambda game: game.get("promotions")
|
||||
and (
|
||||
# Current discount(s)
|
||||
game["promotions"]["promotionalOffers"]
|
||||
or
|
||||
# Upcoming discount(s)
|
||||
game["promotions"]["upcomingPromotionalOffers"]
|
||||
),
|
||||
data,
|
||||
)
|
||||
|
||||
return_data: dict[str, list[dict[str, Any]]] = {
|
||||
CalendarType.DISCOUNT: [],
|
||||
CalendarType.FREE: [],
|
||||
}
|
||||
for discount_game in discount_games:
|
||||
game = format_game_data(discount_game, self.language)
|
||||
|
||||
if game["discount_type"]:
|
||||
return_data[game["discount_type"]].append(game)
|
||||
|
||||
return_data[CalendarType.DISCOUNT] = sorted(
|
||||
return_data[CalendarType.DISCOUNT],
|
||||
key=lambda game: game["discount_start_at"],
|
||||
)
|
||||
return_data[CalendarType.FREE] = sorted(
|
||||
return_data[CalendarType.FREE], key=lambda game: game["discount_start_at"]
|
||||
)
|
||||
|
||||
_LOGGER.debug(return_data)
|
||||
return return_data
|
92
homeassistant/components/epic_games_store/helper.py
Normal file
92
homeassistant/components/epic_games_store/helper.py
Normal file
@ -0,0 +1,92 @@
|
||||
"""Helper for Epic Games Store."""
|
||||
|
||||
import contextlib
|
||||
from typing import Any
|
||||
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
|
||||
def format_game_data(raw_game_data: dict[str, Any], language: str) -> dict[str, Any]:
|
||||
"""Format raw API game data for Home Assistant users."""
|
||||
img_portrait = None
|
||||
img_landscape = None
|
||||
|
||||
for image in raw_game_data["keyImages"]:
|
||||
if image["type"] == "OfferImageTall":
|
||||
img_portrait = image["url"]
|
||||
if image["type"] == "OfferImageWide":
|
||||
img_landscape = image["url"]
|
||||
|
||||
current_promotions = raw_game_data["promotions"]["promotionalOffers"]
|
||||
upcoming_promotions = raw_game_data["promotions"]["upcomingPromotionalOffers"]
|
||||
|
||||
promotion_data = {}
|
||||
if (
|
||||
current_promotions
|
||||
and raw_game_data["price"]["totalPrice"]["discountPrice"] == 0
|
||||
):
|
||||
promotion_data = current_promotions[0]["promotionalOffers"][0]
|
||||
else:
|
||||
promotion_data = (current_promotions or upcoming_promotions)[0][
|
||||
"promotionalOffers"
|
||||
][0]
|
||||
|
||||
return {
|
||||
"title": raw_game_data["title"].replace("\xa0", " "),
|
||||
"description": raw_game_data["description"].strip().replace("\xa0", " "),
|
||||
"released_at": dt_util.parse_datetime(raw_game_data["effectiveDate"]),
|
||||
"original_price": raw_game_data["price"]["totalPrice"]["fmtPrice"][
|
||||
"originalPrice"
|
||||
].replace("\xa0", " "),
|
||||
"publisher": raw_game_data["seller"]["name"],
|
||||
"url": get_game_url(raw_game_data, language),
|
||||
"img_portrait": img_portrait,
|
||||
"img_landscape": img_landscape,
|
||||
"discount_type": ("free" if is_free_game(raw_game_data) else "discount")
|
||||
if promotion_data
|
||||
else None,
|
||||
"discount_start_at": dt_util.parse_datetime(promotion_data["startDate"])
|
||||
if promotion_data
|
||||
else None,
|
||||
"discount_end_at": dt_util.parse_datetime(promotion_data["endDate"])
|
||||
if promotion_data
|
||||
else None,
|
||||
}
|
||||
|
||||
|
||||
def get_game_url(raw_game_data: dict[str, Any], language: str) -> str:
|
||||
"""Format raw API game data for Home Assistant users."""
|
||||
url_bundle_or_product = "bundles" if raw_game_data["offerType"] == "BUNDLE" else "p"
|
||||
url_slug: str | None = None
|
||||
try:
|
||||
url_slug = raw_game_data["offerMappings"][0]["pageSlug"]
|
||||
except Exception: # pylint: disable=broad-except
|
||||
with contextlib.suppress(Exception):
|
||||
url_slug = raw_game_data["catalogNs"]["mappings"][0]["pageSlug"]
|
||||
|
||||
if not url_slug:
|
||||
url_slug = raw_game_data["urlSlug"]
|
||||
|
||||
return f"https://store.epicgames.com/{language}/{url_bundle_or_product}/{url_slug}"
|
||||
|
||||
|
||||
def is_free_game(game: dict[str, Any]) -> bool:
|
||||
"""Return if the game is free or will be free."""
|
||||
return (
|
||||
# Current free game(s)
|
||||
game["promotions"]["promotionalOffers"]
|
||||
and game["promotions"]["promotionalOffers"][0]["promotionalOffers"][0][
|
||||
"discountSetting"
|
||||
]["discountPercentage"]
|
||||
== 0
|
||||
and
|
||||
# Checking current price, maybe not necessary
|
||||
game["price"]["totalPrice"]["discountPrice"] == 0
|
||||
) or (
|
||||
# Upcoming free game(s)
|
||||
game["promotions"]["upcomingPromotionalOffers"]
|
||||
and game["promotions"]["upcomingPromotionalOffers"][0]["promotionalOffers"][0][
|
||||
"discountSetting"
|
||||
]["discountPercentage"]
|
||||
== 0
|
||||
)
|
10
homeassistant/components/epic_games_store/manifest.json
Normal file
10
homeassistant/components/epic_games_store/manifest.json
Normal file
@ -0,0 +1,10 @@
|
||||
{
|
||||
"domain": "epic_games_store",
|
||||
"name": "Epic Games Store",
|
||||
"codeowners": ["@hacf-fr", "@Quentame"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/epic_games_store",
|
||||
"integration_type": "service",
|
||||
"iot_class": "cloud_polling",
|
||||
"requirements": ["epicstore-api==0.1.7"]
|
||||
}
|
38
homeassistant/components/epic_games_store/strings.json
Normal file
38
homeassistant/components/epic_games_store/strings.json
Normal file
@ -0,0 +1,38 @@
|
||||
{
|
||||
"config": {
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"language": "Language",
|
||||
"country": "Country"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||
},
|
||||
"abort": {
|
||||
"already_configured": "[%key:common::config_flow::abort::already_configured_service%]"
|
||||
}
|
||||
},
|
||||
"entity": {
|
||||
"calendar": {
|
||||
"free_games": {
|
||||
"name": "Free games",
|
||||
"state_attributes": {
|
||||
"games": {
|
||||
"name": "Games"
|
||||
}
|
||||
}
|
||||
},
|
||||
"discount_games": {
|
||||
"name": "Discount games",
|
||||
"state_attributes": {
|
||||
"games": {
|
||||
"name": "[%key:component::epic_games_store::entity::calendar::free_games::state_attributes::games::name%]"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -152,6 +152,7 @@ FLOWS = {
|
||||
"enocean",
|
||||
"enphase_envoy",
|
||||
"environment_canada",
|
||||
"epic_games_store",
|
||||
"epion",
|
||||
"epson",
|
||||
"eq3btsmart",
|
||||
|
@ -1649,6 +1649,12 @@
|
||||
"config_flow": false,
|
||||
"iot_class": "local_polling"
|
||||
},
|
||||
"epic_games_store": {
|
||||
"name": "Epic Games Store",
|
||||
"integration_type": "service",
|
||||
"config_flow": true,
|
||||
"iot_class": "cloud_polling"
|
||||
},
|
||||
"epion": {
|
||||
"name": "Epion",
|
||||
"integration_type": "hub",
|
||||
|
@ -806,6 +806,9 @@ env-canada==0.6.0
|
||||
# homeassistant.components.season
|
||||
ephem==4.1.5
|
||||
|
||||
# homeassistant.components.epic_games_store
|
||||
epicstore-api==0.1.7
|
||||
|
||||
# homeassistant.components.epion
|
||||
epion==0.0.3
|
||||
|
||||
|
@ -660,6 +660,9 @@ env-canada==0.6.0
|
||||
# homeassistant.components.season
|
||||
ephem==4.1.5
|
||||
|
||||
# homeassistant.components.epic_games_store
|
||||
epicstore-api==0.1.7
|
||||
|
||||
# homeassistant.components.epion
|
||||
epion==0.0.3
|
||||
|
||||
|
1
tests/components/epic_games_store/__init__.py
Normal file
1
tests/components/epic_games_store/__init__.py
Normal file
@ -0,0 +1 @@
|
||||
"""Tests for the Epic Games Store integration."""
|
31
tests/components/epic_games_store/common.py
Normal file
31
tests/components/epic_games_store/common.py
Normal file
@ -0,0 +1,31 @@
|
||||
"""Common methods used across tests for Epic Games Store."""
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
from homeassistant.components.epic_games_store.const import DOMAIN
|
||||
from homeassistant.const import CONF_COUNTRY, CONF_LANGUAGE
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
from .const import MOCK_COUNTRY, MOCK_LANGUAGE
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
async def setup_platform(hass: HomeAssistant, platform: str) -> MockConfigEntry:
|
||||
"""Set up the Epic Games Store platform."""
|
||||
mock_entry = MockConfigEntry(
|
||||
domain=DOMAIN,
|
||||
data={
|
||||
CONF_LANGUAGE: MOCK_LANGUAGE,
|
||||
CONF_COUNTRY: MOCK_COUNTRY,
|
||||
},
|
||||
unique_id=f"freegames-{MOCK_LANGUAGE}-{MOCK_COUNTRY}",
|
||||
)
|
||||
mock_entry.add_to_hass(hass)
|
||||
|
||||
with patch("homeassistant.components.epic_games_store.PLATFORMS", [platform]):
|
||||
assert await async_setup_component(hass, DOMAIN, {})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
return mock_entry
|
44
tests/components/epic_games_store/conftest.py
Normal file
44
tests/components/epic_games_store/conftest.py
Normal file
@ -0,0 +1,44 @@
|
||||
"""Define fixtures for Epic Games Store tests."""
|
||||
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from .const import (
|
||||
DATA_ERROR_ATTRIBUTE_NOT_FOUND,
|
||||
DATA_FREE_GAMES,
|
||||
DATA_FREE_GAMES_CHRISTMAS_SPECIAL,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture(name="service_multiple")
|
||||
def mock_service_multiple():
|
||||
"""Mock a successful service with multiple free & discount games."""
|
||||
with patch(
|
||||
"homeassistant.components.epic_games_store.coordinator.EpicGamesStoreAPI"
|
||||
) as service_mock:
|
||||
instance = service_mock.return_value
|
||||
instance.get_free_games = Mock(return_value=DATA_FREE_GAMES)
|
||||
yield service_mock
|
||||
|
||||
|
||||
@pytest.fixture(name="service_christmas_special")
|
||||
def mock_service_christmas_special():
|
||||
"""Mock a successful service with Christmas special case."""
|
||||
with patch(
|
||||
"homeassistant.components.epic_games_store.coordinator.EpicGamesStoreAPI"
|
||||
) as service_mock:
|
||||
instance = service_mock.return_value
|
||||
instance.get_free_games = Mock(return_value=DATA_FREE_GAMES_CHRISTMAS_SPECIAL)
|
||||
yield service_mock
|
||||
|
||||
|
||||
@pytest.fixture(name="service_attribute_not_found")
|
||||
def mock_service_attribute_not_found():
|
||||
"""Mock a successful service returning a not found attribute error with free & discount games."""
|
||||
with patch(
|
||||
"homeassistant.components.epic_games_store.coordinator.EpicGamesStoreAPI"
|
||||
) as service_mock:
|
||||
instance = service_mock.return_value
|
||||
instance.get_free_games = Mock(return_value=DATA_ERROR_ATTRIBUTE_NOT_FOUND)
|
||||
yield service_mock
|
25
tests/components/epic_games_store/const.py
Normal file
25
tests/components/epic_games_store/const.py
Normal file
@ -0,0 +1,25 @@
|
||||
"""Test constants."""
|
||||
|
||||
from homeassistant.components.epic_games_store.const import DOMAIN
|
||||
|
||||
from tests.common import load_json_object_fixture
|
||||
|
||||
MOCK_LANGUAGE = "fr"
|
||||
MOCK_COUNTRY = "FR"
|
||||
|
||||
DATA_ERROR_ATTRIBUTE_NOT_FOUND = load_json_object_fixture(
|
||||
"error_1004_attribute_not_found.json", DOMAIN
|
||||
)
|
||||
|
||||
DATA_ERROR_WRONG_COUNTRY = load_json_object_fixture(
|
||||
"error_5222_wrong_country.json", DOMAIN
|
||||
)
|
||||
|
||||
# free games
|
||||
DATA_FREE_GAMES = load_json_object_fixture("free_games.json", DOMAIN)
|
||||
|
||||
DATA_FREE_GAMES_ONE = load_json_object_fixture("free_games_one.json", DOMAIN)
|
||||
|
||||
DATA_FREE_GAMES_CHRISTMAS_SPECIAL = load_json_object_fixture(
|
||||
"free_games_christmas_special.json", DOMAIN
|
||||
)
|
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,23 @@
|
||||
{
|
||||
"errors": [
|
||||
{
|
||||
"message": "CatalogQuery/searchStore: Request failed with status code 400",
|
||||
"locations": [
|
||||
{
|
||||
"line": 18,
|
||||
"column": 9
|
||||
}
|
||||
],
|
||||
"correlationId": "e10ad58e-a4f9-4097-af5d-cafdbe0d8bbd",
|
||||
"serviceResponse": "{\"errorCode\":\"errors.com.epicgames.catalog.invalid_country_code\",\"errorMessage\":\"Sorry the value you entered: en-US, does not appear to be a valid ISO country code.\",\"messageVars\":[\"en-US\"],\"numericErrorCode\":5222,\"originatingService\":\"com.epicgames.catalog.public\",\"intent\":\"prod\",\"errorStatus\":400}",
|
||||
"stack": null,
|
||||
"path": ["Catalog", "searchStore"]
|
||||
}
|
||||
],
|
||||
"data": {
|
||||
"Catalog": {
|
||||
"searchStore": null
|
||||
}
|
||||
},
|
||||
"extensions": {}
|
||||
}
|
2189
tests/components/epic_games_store/fixtures/free_games.json
Normal file
2189
tests/components/epic_games_store/fixtures/free_games.json
Normal file
File diff suppressed because it is too large
Load Diff
@ -0,0 +1,253 @@
|
||||
{
|
||||
"data": {
|
||||
"Catalog": {
|
||||
"searchStore": {
|
||||
"elements": [
|
||||
{
|
||||
"title": "Cursed to Golf",
|
||||
"id": "0e4551e4ae65492b88009f8a4e41d778",
|
||||
"namespace": "d5241c76f178492ea1540fce45616757",
|
||||
"description": "Cursed to Golf",
|
||||
"effectiveDate": "2023-12-27T16:00:00.000Z",
|
||||
"offerType": "OTHERS",
|
||||
"expiryDate": "2023-12-28T16:00:00.000Z",
|
||||
"viewableDate": "2023-12-26T15:25:00.000Z",
|
||||
"status": "ACTIVE",
|
||||
"isCodeRedemptionOnly": true,
|
||||
"keyImages": [
|
||||
{
|
||||
"type": "DieselStoreFrontWide",
|
||||
"url": "https://cdn1.epicgames.com/offer/d5241c76f178492ea1540fce45616757/Free-Game-9_1920x1080-418a8fa10dd305bb2a219a7ec869c5ef"
|
||||
},
|
||||
{
|
||||
"type": "VaultClosed",
|
||||
"url": "https://cdn1.epicgames.com/offer/d5241c76f178492ea1540fce45616757/Free-Game-9-teaser_1920x1080-e71ae0041736db5ac259a355cb301116"
|
||||
}
|
||||
],
|
||||
"seller": {
|
||||
"id": "o-ufmrk5furrrxgsp5tdngefzt5rxdcn",
|
||||
"name": "Epic Dev Test Account"
|
||||
},
|
||||
"productSlug": "cursed-to-golf-a6bc22",
|
||||
"urlSlug": "mysterygame-9",
|
||||
"url": null,
|
||||
"items": [
|
||||
{
|
||||
"id": "8341d7c7e4534db7848cc428aa4cbe5a",
|
||||
"namespace": "d5241c76f178492ea1540fce45616757"
|
||||
}
|
||||
],
|
||||
"customAttributes": [
|
||||
{
|
||||
"key": "com.epicgames.app.freegames.vault.close",
|
||||
"value": "[]"
|
||||
},
|
||||
{
|
||||
"key": "com.epicgames.app.blacklist",
|
||||
"value": "[]"
|
||||
},
|
||||
{
|
||||
"key": "com.epicgames.app.freegames.vault.slug",
|
||||
"value": "sales-and-specials/holiday-sale"
|
||||
},
|
||||
{
|
||||
"key": "com.epicgames.app.freegames.vault.open",
|
||||
"value": "[]"
|
||||
},
|
||||
{
|
||||
"key": "com.epicgames.app.productSlug",
|
||||
"value": "cursed-to-golf-a6bc22"
|
||||
}
|
||||
],
|
||||
"categories": [
|
||||
{
|
||||
"path": "freegames/vaulted"
|
||||
},
|
||||
{
|
||||
"path": "freegames"
|
||||
},
|
||||
{
|
||||
"path": "games"
|
||||
},
|
||||
{
|
||||
"path": "applications"
|
||||
}
|
||||
],
|
||||
"tags": [],
|
||||
"catalogNs": {
|
||||
"mappings": []
|
||||
},
|
||||
"offerMappings": [],
|
||||
"price": {
|
||||
"totalPrice": {
|
||||
"discountPrice": 0,
|
||||
"originalPrice": 0,
|
||||
"voucherDiscount": 0,
|
||||
"discount": 0,
|
||||
"currencyCode": "EUR",
|
||||
"currencyInfo": {
|
||||
"decimals": 2
|
||||
},
|
||||
"fmtPrice": {
|
||||
"originalPrice": "0",
|
||||
"discountPrice": "0",
|
||||
"intermediatePrice": "0"
|
||||
}
|
||||
},
|
||||
"lineOffers": [
|
||||
{
|
||||
"appliedRules": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"promotions": {
|
||||
"promotionalOffers": [
|
||||
{
|
||||
"promotionalOffers": [
|
||||
{
|
||||
"startDate": "2023-12-27T16:00:00.000Z",
|
||||
"endDate": "2023-12-28T16:00:00.000Z",
|
||||
"discountSetting": {
|
||||
"discountType": "PERCENTAGE",
|
||||
"discountPercentage": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"startDate": "2023-12-27T16:00:00.000Z",
|
||||
"endDate": "2023-12-28T16:00:00.000Z",
|
||||
"discountSetting": {
|
||||
"discountType": "PERCENTAGE",
|
||||
"discountPercentage": 0
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"upcomingPromotionalOffers": []
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "Mystery Game Day 10",
|
||||
"id": "a8c3537a579943a688e3bd355ae36209",
|
||||
"namespace": "d5241c76f178492ea1540fce45616757",
|
||||
"description": "Mystery Game Day 10",
|
||||
"effectiveDate": "2099-01-01T16:00:00.000Z",
|
||||
"offerType": "OTHERS",
|
||||
"expiryDate": null,
|
||||
"viewableDate": "2023-12-27T15:25:00.000Z",
|
||||
"status": "ACTIVE",
|
||||
"isCodeRedemptionOnly": true,
|
||||
"keyImages": [
|
||||
{
|
||||
"type": "VaultClosed",
|
||||
"url": "https://cdn1.epicgames.com/offer/d5241c76f178492ea1540fce45616757/Free-Game-10-teaser_1920x1080-3ea48042a44263bf1a0a59c725b6d95b"
|
||||
},
|
||||
{
|
||||
"type": "DieselStoreFrontWide",
|
||||
"url": "https://cdn1.epicgames.com/offer/d5241c76f178492ea1540fce45616757/Free-Game-10-teaser_1920x1080-3ea48042a44263bf1a0a59c725b6d95b"
|
||||
}
|
||||
],
|
||||
"seller": {
|
||||
"id": "o-ufmrk5furrrxgsp5tdngefzt5rxdcn",
|
||||
"name": "Epic Dev Test Account"
|
||||
},
|
||||
"productSlug": "[]",
|
||||
"urlSlug": "mysterygame-10",
|
||||
"url": null,
|
||||
"items": [
|
||||
{
|
||||
"id": "8341d7c7e4534db7848cc428aa4cbe5a",
|
||||
"namespace": "d5241c76f178492ea1540fce45616757"
|
||||
}
|
||||
],
|
||||
"customAttributes": [
|
||||
{
|
||||
"key": "com.epicgames.app.freegames.vault.close",
|
||||
"value": "[]"
|
||||
},
|
||||
{
|
||||
"key": "com.epicgames.app.blacklist",
|
||||
"value": "[]"
|
||||
},
|
||||
{
|
||||
"key": "com.epicgames.app.freegames.vault.slug",
|
||||
"value": "sales-and-specials/holiday-sale"
|
||||
},
|
||||
{
|
||||
"key": "com.epicgames.app.freegames.vault.open",
|
||||
"value": "[]"
|
||||
},
|
||||
{
|
||||
"key": "com.epicgames.app.productSlug",
|
||||
"value": "[]"
|
||||
}
|
||||
],
|
||||
"categories": [
|
||||
{
|
||||
"path": "freegames/vaulted"
|
||||
},
|
||||
{
|
||||
"path": "freegames"
|
||||
},
|
||||
{
|
||||
"path": "games"
|
||||
},
|
||||
{
|
||||
"path": "applications"
|
||||
}
|
||||
],
|
||||
"tags": [],
|
||||
"catalogNs": {
|
||||
"mappings": []
|
||||
},
|
||||
"offerMappings": [],
|
||||
"price": {
|
||||
"totalPrice": {
|
||||
"discountPrice": 0,
|
||||
"originalPrice": 0,
|
||||
"voucherDiscount": 0,
|
||||
"discount": 0,
|
||||
"currencyCode": "EUR",
|
||||
"currencyInfo": {
|
||||
"decimals": 2
|
||||
},
|
||||
"fmtPrice": {
|
||||
"originalPrice": "0",
|
||||
"discountPrice": "0",
|
||||
"intermediatePrice": "0"
|
||||
}
|
||||
},
|
||||
"lineOffers": [
|
||||
{
|
||||
"appliedRules": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"promotions": {
|
||||
"promotionalOffers": [],
|
||||
"upcomingPromotionalOffers": [
|
||||
{
|
||||
"promotionalOffers": [
|
||||
{
|
||||
"startDate": "2023-12-28T16:00:00.000Z",
|
||||
"endDate": "2023-12-29T16:00:00.000Z",
|
||||
"discountSetting": {
|
||||
"discountType": "PERCENTAGE",
|
||||
"discountPercentage": 0
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"paging": {
|
||||
"count": 1000,
|
||||
"total": 2
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"extensions": {}
|
||||
}
|
658
tests/components/epic_games_store/fixtures/free_games_one.json
Normal file
658
tests/components/epic_games_store/fixtures/free_games_one.json
Normal file
@ -0,0 +1,658 @@
|
||||
{
|
||||
"data": {
|
||||
"Catalog": {
|
||||
"searchStore": {
|
||||
"elements": [
|
||||
{
|
||||
"title": "Borderlands 3 Season Pass",
|
||||
"id": "c3913a91e07b43cfbbbcfd8244c86dcc",
|
||||
"namespace": "catnip",
|
||||
"description": "Prolongez votre aventure dans Borderlands\u00a03 avec le Season Pass, regroupant des \u00e9l\u00e9ments cosm\u00e9tiques exclusifs et quatre histoires additionnelles, pour encore plus de missions et de d\u00e9fis\u00a0!",
|
||||
"effectiveDate": "2019-09-11T12:00:00.000Z",
|
||||
"offerType": "DLC",
|
||||
"expiryDate": null,
|
||||
"status": "ACTIVE",
|
||||
"isCodeRedemptionOnly": false,
|
||||
"keyImages": [
|
||||
{
|
||||
"type": "OfferImageWide",
|
||||
"url": "https://cdn1.epicgames.com/offer/catnip/Diesel_productv2_borderlands-3_season-pass_BL3_SEASONPASS_Hero-3840x2160-4411e63a005a43811a2bc516ae7ec584598fd4aa-3840x2160-b8988ebb0f3d9159671e8968af991f30_3840x2160-b8988ebb0f3d9159671e8968af991f30"
|
||||
},
|
||||
{
|
||||
"type": "OfferImageTall",
|
||||
"url": "https://cdn1.epicgames.com/offer/catnip/2KGMKT_BL3_Season_Pass_EGS_1200x1600_1200x1600-a7438a079c5576d328a74b9121278075"
|
||||
},
|
||||
{
|
||||
"type": "CodeRedemption_340x440",
|
||||
"url": "https://cdn1.epicgames.com/offer/catnip/2KGMKT_BL3_Season_Pass_EGS_1200x1600_1200x1600-a7438a079c5576d328a74b9121278075"
|
||||
},
|
||||
{
|
||||
"type": "Thumbnail",
|
||||
"url": "https://cdn1.epicgames.com/offer/catnip/2KGMKT_BL3_Season_Pass_EGS_1200x1600_1200x1600-a7438a079c5576d328a74b9121278075"
|
||||
}
|
||||
],
|
||||
"seller": {
|
||||
"id": "o-37m6jbj5wcvrcvm4wusv7nazdfvbjk",
|
||||
"name": "2K Games, Inc."
|
||||
},
|
||||
"productSlug": "borderlands-3/season-pass",
|
||||
"urlSlug": "borderlands-3--season-pass",
|
||||
"url": null,
|
||||
"items": [
|
||||
{
|
||||
"id": "e9fdc1a9f47b4a5e8e63841c15de2b12",
|
||||
"namespace": "catnip"
|
||||
},
|
||||
{
|
||||
"id": "fbc46bb6056940d2847ee1e80037a9af",
|
||||
"namespace": "catnip"
|
||||
},
|
||||
{
|
||||
"id": "ff8e1152ddf742b68f9ac0cecd378917",
|
||||
"namespace": "catnip"
|
||||
},
|
||||
{
|
||||
"id": "939e660825764e208938ab4f26b4da56",
|
||||
"namespace": "catnip"
|
||||
},
|
||||
{
|
||||
"id": "4c43a9a691114ccd91c1884ab18f4e27",
|
||||
"namespace": "catnip"
|
||||
},
|
||||
{
|
||||
"id": "3a6a3f9b351b4b599808df3267669b83",
|
||||
"namespace": "catnip"
|
||||
},
|
||||
{
|
||||
"id": "ab030a9f53f3428fb2baf2ddbb0bb5ac",
|
||||
"namespace": "catnip"
|
||||
},
|
||||
{
|
||||
"id": "ff96eef22b0e4c498e8ed80ac0030325",
|
||||
"namespace": "catnip"
|
||||
},
|
||||
{
|
||||
"id": "5021e93a73374d6db1c1ce6c92234f8f",
|
||||
"namespace": "catnip"
|
||||
},
|
||||
{
|
||||
"id": "9c0b1eb3265340678dff0fcb106402b1",
|
||||
"namespace": "catnip"
|
||||
},
|
||||
{
|
||||
"id": "8c826db6e14f44aeac8816e1bd593632",
|
||||
"namespace": "catnip"
|
||||
}
|
||||
],
|
||||
"customAttributes": [
|
||||
{
|
||||
"key": "com.epicgames.app.blacklist",
|
||||
"value": "SA"
|
||||
},
|
||||
{
|
||||
"key": "publisherName",
|
||||
"value": "2K"
|
||||
},
|
||||
{
|
||||
"key": "developerName",
|
||||
"value": "Gearbox Software"
|
||||
},
|
||||
{
|
||||
"key": "com.epicgames.app.productSlug",
|
||||
"value": "borderlands-3/season-pass"
|
||||
}
|
||||
],
|
||||
"categories": [
|
||||
{
|
||||
"path": "addons"
|
||||
},
|
||||
{
|
||||
"path": "freegames"
|
||||
},
|
||||
{
|
||||
"path": "addons/durable"
|
||||
},
|
||||
{
|
||||
"path": "applications"
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
{
|
||||
"id": "1264"
|
||||
},
|
||||
{
|
||||
"id": "16004"
|
||||
},
|
||||
{
|
||||
"id": "14869"
|
||||
},
|
||||
{
|
||||
"id": "26789"
|
||||
},
|
||||
{
|
||||
"id": "1367"
|
||||
},
|
||||
{
|
||||
"id": "1370"
|
||||
},
|
||||
{
|
||||
"id": "9547"
|
||||
},
|
||||
{
|
||||
"id": "9549"
|
||||
},
|
||||
{
|
||||
"id": "1294"
|
||||
}
|
||||
],
|
||||
"catalogNs": {
|
||||
"mappings": [
|
||||
{
|
||||
"pageSlug": "borderlands-3",
|
||||
"pageType": "productHome"
|
||||
}
|
||||
]
|
||||
},
|
||||
"offerMappings": [
|
||||
{
|
||||
"pageSlug": "borderlands-3--season-pass",
|
||||
"pageType": "addon--cms-hybrid"
|
||||
}
|
||||
],
|
||||
"price": {
|
||||
"totalPrice": {
|
||||
"discountPrice": 4999,
|
||||
"originalPrice": 4999,
|
||||
"voucherDiscount": 0,
|
||||
"discount": 0,
|
||||
"currencyCode": "EUR",
|
||||
"currencyInfo": {
|
||||
"decimals": 2
|
||||
},
|
||||
"fmtPrice": {
|
||||
"originalPrice": "49,99\u00a0\u20ac",
|
||||
"discountPrice": "49,99\u00a0\u20ac",
|
||||
"intermediatePrice": "49,99\u00a0\u20ac"
|
||||
}
|
||||
},
|
||||
"lineOffers": [
|
||||
{
|
||||
"appliedRules": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"promotions": {
|
||||
"promotionalOffers": [],
|
||||
"upcomingPromotionalOffers": [
|
||||
{
|
||||
"promotionalOffers": [
|
||||
{
|
||||
"startDate": "2023-03-09T16:00:00.000Z",
|
||||
"endDate": "2023-03-16T16:00:00.000Z",
|
||||
"discountSetting": {
|
||||
"discountType": "PERCENTAGE",
|
||||
"discountPercentage": 30
|
||||
}
|
||||
},
|
||||
{
|
||||
"startDate": "2023-03-09T16:00:00.000Z",
|
||||
"endDate": "2023-03-16T16:00:00.000Z",
|
||||
"discountSetting": {
|
||||
"discountType": "PERCENTAGE",
|
||||
"discountPercentage": 25
|
||||
}
|
||||
},
|
||||
{
|
||||
"startDate": "2023-03-09T16:00:00.000Z",
|
||||
"endDate": "2023-03-16T16:00:00.000Z",
|
||||
"discountSetting": {
|
||||
"discountType": "PERCENTAGE",
|
||||
"discountPercentage": 25
|
||||
}
|
||||
},
|
||||
{
|
||||
"startDate": "2023-03-09T16:00:00.000Z",
|
||||
"endDate": "2023-03-16T16:00:00.000Z",
|
||||
"discountSetting": {
|
||||
"discountType": "PERCENTAGE",
|
||||
"discountPercentage": 30
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "Call of the Sea",
|
||||
"id": "92da5d8d918543b6b408e36d9af81765",
|
||||
"namespace": "5e427319eea1401ab20c6cd78a4163c4",
|
||||
"description": "Call of the Sea is an otherworldly tale of mystery and love set in the 1930s South Pacific. Explore a lush island paradise, solve puzzles and unlock secrets in the hunt for your husband\u2019s missing expedition.",
|
||||
"effectiveDate": "2022-02-17T15:00:00.000Z",
|
||||
"offerType": "BASE_GAME",
|
||||
"expiryDate": null,
|
||||
"status": "ACTIVE",
|
||||
"isCodeRedemptionOnly": false,
|
||||
"keyImages": [
|
||||
{
|
||||
"type": "DieselStoreFrontWide",
|
||||
"url": "https://cdn1.epicgames.com/salesEvent/salesEvent/EGS_CalloftheSea_OutoftheBlue_S1_2560x1440-204699c6410deef9c18be0ee392f8335"
|
||||
},
|
||||
{
|
||||
"type": "DieselStoreFrontTall",
|
||||
"url": "https://cdn1.epicgames.com/salesEvent/salesEvent/EGS_CalloftheSea_OutoftheBlue_S2_1200x1600-db63acf0c479c185e0ef8f8e73c8f0d8"
|
||||
},
|
||||
{
|
||||
"type": "OfferImageWide",
|
||||
"url": "https://cdn1.epicgames.com/salesEvent/salesEvent/EGS_CalloftheSea_OutoftheBlue_S5_1920x1080-7b22dfebdd9fcdde6e526c5dc4c16eb1"
|
||||
},
|
||||
{
|
||||
"type": "OfferImageTall",
|
||||
"url": "https://cdn1.epicgames.com/salesEvent/salesEvent/EGS_CalloftheSea_OutoftheBlue_S2_1200x1600-db63acf0c479c185e0ef8f8e73c8f0d8"
|
||||
},
|
||||
{
|
||||
"type": "CodeRedemption_340x440",
|
||||
"url": "https://cdn1.epicgames.com/salesEvent/salesEvent/EGS_CalloftheSea_OutoftheBlue_S2_1200x1600-db63acf0c479c185e0ef8f8e73c8f0d8"
|
||||
},
|
||||
{
|
||||
"type": "Thumbnail",
|
||||
"url": "https://cdn1.epicgames.com/salesEvent/salesEvent/EGS_CalloftheSea_OutoftheBlue_S2_1200x1600-db63acf0c479c185e0ef8f8e73c8f0d8"
|
||||
}
|
||||
],
|
||||
"seller": {
|
||||
"id": "o-fay4ghw9hhamujs53rfhy83ffexb7k",
|
||||
"name": "Raw Fury"
|
||||
},
|
||||
"productSlug": "call-of-the-sea",
|
||||
"urlSlug": "call-of-the-sea",
|
||||
"url": null,
|
||||
"items": [
|
||||
{
|
||||
"id": "cbc9c76c4bfc4bc6b28abb3afbcbf07a",
|
||||
"namespace": "5e427319eea1401ab20c6cd78a4163c4"
|
||||
}
|
||||
],
|
||||
"customAttributes": [
|
||||
{
|
||||
"key": "com.epicgames.app.productSlug",
|
||||
"value": "call-of-the-sea"
|
||||
}
|
||||
],
|
||||
"categories": [
|
||||
{
|
||||
"path": "freegames"
|
||||
},
|
||||
{
|
||||
"path": "games"
|
||||
},
|
||||
{
|
||||
"path": "games/edition"
|
||||
},
|
||||
{
|
||||
"path": "games/edition/base"
|
||||
},
|
||||
{
|
||||
"path": "applications"
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
{
|
||||
"id": "1296"
|
||||
},
|
||||
{
|
||||
"id": "1298"
|
||||
},
|
||||
{
|
||||
"id": "21894"
|
||||
},
|
||||
{
|
||||
"id": "1370"
|
||||
},
|
||||
{
|
||||
"id": "9547"
|
||||
},
|
||||
{
|
||||
"id": "1117"
|
||||
}
|
||||
],
|
||||
"catalogNs": {
|
||||
"mappings": [
|
||||
{
|
||||
"pageSlug": "call-of-the-sea",
|
||||
"pageType": "productHome"
|
||||
}
|
||||
]
|
||||
},
|
||||
"offerMappings": [],
|
||||
"price": {
|
||||
"totalPrice": {
|
||||
"discountPrice": 1999,
|
||||
"originalPrice": 1999,
|
||||
"voucherDiscount": 0,
|
||||
"discount": 0,
|
||||
"currencyCode": "EUR",
|
||||
"currencyInfo": {
|
||||
"decimals": 2
|
||||
},
|
||||
"fmtPrice": {
|
||||
"originalPrice": "19,99\u00a0\u20ac",
|
||||
"discountPrice": "19,99\u00a0\u20ac",
|
||||
"intermediatePrice": "19,99\u00a0\u20ac"
|
||||
}
|
||||
},
|
||||
"lineOffers": [
|
||||
{
|
||||
"appliedRules": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"promotions": {
|
||||
"promotionalOffers": [],
|
||||
"upcomingPromotionalOffers": [
|
||||
{
|
||||
"promotionalOffers": [
|
||||
{
|
||||
"startDate": "2023-03-09T16:00:00.000Z",
|
||||
"endDate": "2023-03-16T16:00:00.000Z",
|
||||
"discountSetting": {
|
||||
"discountType": "PERCENTAGE",
|
||||
"discountPercentage": 60
|
||||
}
|
||||
},
|
||||
{
|
||||
"startDate": "2023-03-09T16:00:00.000Z",
|
||||
"endDate": "2023-03-16T16:00:00.000Z",
|
||||
"discountSetting": {
|
||||
"discountType": "PERCENTAGE",
|
||||
"discountPercentage": 0
|
||||
}
|
||||
},
|
||||
{
|
||||
"startDate": "2023-03-09T16:00:00.000Z",
|
||||
"endDate": "2023-03-16T16:00:00.000Z",
|
||||
"discountSetting": {
|
||||
"discountType": "PERCENTAGE",
|
||||
"discountPercentage": 60
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "Rise of Industry",
|
||||
"id": "c04a2ab8ff4442cba0a41fb83453e701",
|
||||
"namespace": "9f101e25b1a9427a9e6971d2b21c5f82",
|
||||
"description": "Mettez vos comp\u00e9tences entrepreneuriales \u00e0 l'\u00e9preuve en cr\u00e9ant et en optimisant des cha\u00eenes de production complexes tout en gardant un \u0153il sur les r\u00e9sultats financiers. \u00c0 l'aube du 20e si\u00e8cle, appr\u00eatez-vous \u00e0 entrer dans un \u00e2ge d'or industriel, ou une d\u00e9pression historique.",
|
||||
"effectiveDate": "2022-08-11T11:00:00.000Z",
|
||||
"offerType": "BASE_GAME",
|
||||
"expiryDate": null,
|
||||
"status": "ACTIVE",
|
||||
"isCodeRedemptionOnly": false,
|
||||
"keyImages": [
|
||||
{
|
||||
"type": "OfferImageWide",
|
||||
"url": "https://cdn1.epicgames.com/spt-assets/a6aeec29591b4b56b4383b4d2d7d0e1e/rise-of-industry-offer-1p22f.jpg"
|
||||
},
|
||||
{
|
||||
"type": "OfferImageTall",
|
||||
"url": "https://cdn1.epicgames.com/spt-assets/a6aeec29591b4b56b4383b4d2d7d0e1e/download-rise-of-industry-offer-1uujr.jpg"
|
||||
},
|
||||
{
|
||||
"type": "Thumbnail",
|
||||
"url": "https://cdn1.epicgames.com/spt-assets/a6aeec29591b4b56b4383b4d2d7d0e1e/download-rise-of-industry-offer-1uujr.jpg"
|
||||
}
|
||||
],
|
||||
"seller": {
|
||||
"id": "o-fnqgc5v2xczx9fgawvcejwj88z2mnx",
|
||||
"name": "Kasedo Games Ltd"
|
||||
},
|
||||
"productSlug": null,
|
||||
"urlSlug": "f88fedc022fe488caaedaa5c782ff90d",
|
||||
"url": null,
|
||||
"items": [
|
||||
{
|
||||
"id": "9f5b48a778824e6aa330d2c1a47f41b2",
|
||||
"namespace": "9f101e25b1a9427a9e6971d2b21c5f82"
|
||||
}
|
||||
],
|
||||
"customAttributes": [
|
||||
{
|
||||
"key": "autoGeneratedPrice",
|
||||
"value": "false"
|
||||
},
|
||||
{
|
||||
"key": "isManuallySetPCReleaseDate",
|
||||
"value": "true"
|
||||
}
|
||||
],
|
||||
"categories": [
|
||||
{
|
||||
"path": "freegames"
|
||||
},
|
||||
{
|
||||
"path": "games/edition/base"
|
||||
},
|
||||
{
|
||||
"path": "games/edition"
|
||||
},
|
||||
{
|
||||
"path": "games"
|
||||
}
|
||||
],
|
||||
"tags": [
|
||||
{
|
||||
"id": "26789"
|
||||
},
|
||||
{
|
||||
"id": "19847"
|
||||
},
|
||||
{
|
||||
"id": "1370"
|
||||
},
|
||||
{
|
||||
"id": "1115"
|
||||
},
|
||||
{
|
||||
"id": "9547"
|
||||
},
|
||||
{
|
||||
"id": "10719"
|
||||
}
|
||||
],
|
||||
"catalogNs": {
|
||||
"mappings": [
|
||||
{
|
||||
"pageSlug": "rise-of-industry-0af838",
|
||||
"pageType": "productHome"
|
||||
}
|
||||
]
|
||||
},
|
||||
"offerMappings": [
|
||||
{
|
||||
"pageSlug": "rise-of-industry-0af838",
|
||||
"pageType": "productHome"
|
||||
}
|
||||
],
|
||||
"price": {
|
||||
"totalPrice": {
|
||||
"discountPrice": 0,
|
||||
"originalPrice": 2999,
|
||||
"voucherDiscount": 0,
|
||||
"discount": 2999,
|
||||
"currencyCode": "EUR",
|
||||
"currencyInfo": {
|
||||
"decimals": 2
|
||||
},
|
||||
"fmtPrice": {
|
||||
"originalPrice": "29,99\u00a0\u20ac",
|
||||
"discountPrice": "0",
|
||||
"intermediatePrice": "0"
|
||||
}
|
||||
},
|
||||
"lineOffers": [
|
||||
{
|
||||
"appliedRules": [
|
||||
{
|
||||
"id": "a19d30dc34f44923993e68b82b75a084",
|
||||
"endDate": "2023-03-09T16:00:00.000Z",
|
||||
"discountSetting": {
|
||||
"discountType": "PERCENTAGE"
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"promotions": {
|
||||
"promotionalOffers": [
|
||||
{
|
||||
"promotionalOffers": [
|
||||
{
|
||||
"startDate": "2023-03-02T16:00:00.000Z",
|
||||
"endDate": "2023-03-09T16:00:00.000Z",
|
||||
"discountSetting": {
|
||||
"discountType": "PERCENTAGE",
|
||||
"discountPercentage": 0
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"upcomingPromotionalOffers": [
|
||||
{
|
||||
"promotionalOffers": [
|
||||
{
|
||||
"startDate": "2023-03-09T16:00:00.000Z",
|
||||
"endDate": "2023-03-16T16:00:00.000Z",
|
||||
"discountSetting": {
|
||||
"discountType": "PERCENTAGE",
|
||||
"discountPercentage": 25
|
||||
}
|
||||
},
|
||||
{
|
||||
"startDate": "2023-03-09T16:00:00.000Z",
|
||||
"endDate": "2023-03-16T16:00:00.000Z",
|
||||
"discountSetting": {
|
||||
"discountType": "PERCENTAGE",
|
||||
"discountPercentage": 25
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"title": "Dishonored - Definitive Edition",
|
||||
"id": "4d25d74b88d1474a8ab21ffb88ca6d37",
|
||||
"namespace": "d5241c76f178492ea1540fce45616757",
|
||||
"description": "Experience the definitive Dishonored collection. This complete compilation includes Dishonored as well as all of its additional content - Dunwall City Trials, The Knife of Dunwall, The Brigmore Witches and Void Walker\u2019s Arsenal.",
|
||||
"effectiveDate": "2099-01-01T00:00:00.000Z",
|
||||
"offerType": "OTHERS",
|
||||
"expiryDate": null,
|
||||
"status": "ACTIVE",
|
||||
"isCodeRedemptionOnly": true,
|
||||
"keyImages": [
|
||||
{
|
||||
"type": "VaultClosed",
|
||||
"url": "https://cdn1.epicgames.com/offer/d5241c76f178492ea1540fce45616757/15days-day15-wrapped-desktop-carousel-image_1920x1080-ebecfa7c79f02a9de5bca79560bee953"
|
||||
},
|
||||
{
|
||||
"type": "DieselStoreFrontWide",
|
||||
"url": "https://cdn1.epicgames.com/offer/d5241c76f178492ea1540fce45616757/15days-day15-Unwrapped-desktop-carousel-image1_1920x1080-1992edb42bb8554ddeb14d430ba3f858"
|
||||
},
|
||||
{
|
||||
"type": "DieselStoreFrontTall",
|
||||
"url": "https://cdn1.epicgames.com/offer/d5241c76f178492ea1540fce45616757/DAY15-carousel-mobile-unwrapped-image1_1200x1600-9716d77667d2a82931c55a4e4130989e"
|
||||
}
|
||||
],
|
||||
"seller": {
|
||||
"id": "o-ufmrk5furrrxgsp5tdngefzt5rxdcn",
|
||||
"name": "Epic Dev Test Account"
|
||||
},
|
||||
"productSlug": "dishonored-definitive-edition",
|
||||
"urlSlug": "mystery-game15",
|
||||
"url": null,
|
||||
"items": [
|
||||
{
|
||||
"id": "8341d7c7e4534db7848cc428aa4cbe5a",
|
||||
"namespace": "d5241c76f178492ea1540fce45616757"
|
||||
}
|
||||
],
|
||||
"customAttributes": [
|
||||
{
|
||||
"key": "com.epicgames.app.freegames.vault.close",
|
||||
"value": "[]"
|
||||
},
|
||||
{
|
||||
"key": "com.epicgames.app.freegames.vault.slug",
|
||||
"value": "sales-and-specials/holiday-sale"
|
||||
},
|
||||
{
|
||||
"key": "com.epicgames.app.blacklist",
|
||||
"value": "[]"
|
||||
},
|
||||
{
|
||||
"key": "com.epicgames.app.freegames.vault.open",
|
||||
"value": "[]"
|
||||
},
|
||||
{
|
||||
"key": "com.epicgames.app.productSlug",
|
||||
"value": "dishonored-definitive-edition"
|
||||
}
|
||||
],
|
||||
"categories": [
|
||||
{
|
||||
"path": "freegames/vaulted"
|
||||
},
|
||||
{
|
||||
"path": "freegames"
|
||||
},
|
||||
{
|
||||
"path": "games"
|
||||
},
|
||||
{
|
||||
"path": "applications"
|
||||
}
|
||||
],
|
||||
"tags": [],
|
||||
"catalogNs": {
|
||||
"mappings": []
|
||||
},
|
||||
"offerMappings": [],
|
||||
"price": {
|
||||
"totalPrice": {
|
||||
"discountPrice": 0,
|
||||
"originalPrice": 0,
|
||||
"voucherDiscount": 0,
|
||||
"discount": 0,
|
||||
"currencyCode": "EUR",
|
||||
"currencyInfo": {
|
||||
"decimals": 2
|
||||
},
|
||||
"fmtPrice": {
|
||||
"originalPrice": "0",
|
||||
"discountPrice": "0",
|
||||
"intermediatePrice": "0"
|
||||
}
|
||||
},
|
||||
"lineOffers": [
|
||||
{
|
||||
"appliedRules": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"promotions": null
|
||||
}
|
||||
],
|
||||
"paging": {
|
||||
"count": 1000,
|
||||
"total": 4
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"extensions": {}
|
||||
}
|
162
tests/components/epic_games_store/test_calendar.py
Normal file
162
tests/components/epic_games_store/test_calendar.py
Normal file
@ -0,0 +1,162 @@
|
||||
"""Tests for the Epic Games Store calendars."""
|
||||
|
||||
from unittest.mock import Mock
|
||||
|
||||
from freezegun.api import FrozenDateTimeFactory
|
||||
|
||||
from homeassistant.components.calendar import (
|
||||
DOMAIN as CALENDAR_DOMAIN,
|
||||
EVENT_END_DATETIME,
|
||||
EVENT_START_DATETIME,
|
||||
SERVICE_GET_EVENTS,
|
||||
)
|
||||
from homeassistant.const import ATTR_ENTITY_ID, STATE_OFF, STATE_ON
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from .common import setup_platform
|
||||
|
||||
from tests.common import async_fire_time_changed
|
||||
|
||||
|
||||
async def test_setup_component(hass: HomeAssistant, service_multiple: Mock) -> None:
|
||||
"""Test setup component."""
|
||||
await setup_platform(hass, CALENDAR_DOMAIN)
|
||||
|
||||
state = hass.states.get("calendar.epic_games_store_discount_games")
|
||||
assert state.name == "Epic Games Store Discount games"
|
||||
state = hass.states.get("calendar.epic_games_store_free_games")
|
||||
assert state.name == "Epic Games Store Free games"
|
||||
|
||||
|
||||
async def test_discount_games(
|
||||
hass: HomeAssistant,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
service_multiple: Mock,
|
||||
) -> None:
|
||||
"""Test discount games calendar."""
|
||||
freezer.move_to("2022-10-15T00:00:00.000Z")
|
||||
|
||||
await setup_platform(hass, CALENDAR_DOMAIN)
|
||||
|
||||
state = hass.states.get("calendar.epic_games_store_discount_games")
|
||||
assert state.state == STATE_OFF
|
||||
|
||||
freezer.move_to("2022-10-30T00:00:00.000Z")
|
||||
async_fire_time_changed(hass)
|
||||
|
||||
state = hass.states.get("calendar.epic_games_store_discount_games")
|
||||
assert state.state == STATE_ON
|
||||
|
||||
cal_attrs = dict(state.attributes)
|
||||
assert cal_attrs == {
|
||||
"friendly_name": "Epic Games Store Discount games",
|
||||
"message": "Shadow of the Tomb Raider: Definitive Edition",
|
||||
"all_day": False,
|
||||
"start_time": "2022-10-18 08:00:00",
|
||||
"end_time": "2022-11-01 08:00:00",
|
||||
"location": "",
|
||||
"description": "In Shadow of the Tomb Raider Definitive Edition experience the final chapter of Lara\u2019s origin as she is forged into the Tomb Raider she is destined to be.\n\nhttps://store.epicgames.com/fr/p/shadow-of-the-tomb-raider",
|
||||
}
|
||||
|
||||
|
||||
async def test_free_games(
|
||||
hass: HomeAssistant,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
service_multiple: Mock,
|
||||
) -> None:
|
||||
"""Test free games calendar."""
|
||||
freezer.move_to("2022-10-30T00:00:00.000Z")
|
||||
|
||||
await setup_platform(hass, CALENDAR_DOMAIN)
|
||||
|
||||
state = hass.states.get("calendar.epic_games_store_free_games")
|
||||
assert state.state == STATE_ON
|
||||
|
||||
cal_attrs = dict(state.attributes)
|
||||
assert cal_attrs == {
|
||||
"friendly_name": "Epic Games Store Free games",
|
||||
"message": "Warhammer 40,000: Mechanicus - Standard Edition",
|
||||
"all_day": False,
|
||||
"start_time": "2022-10-27 08:00:00",
|
||||
"end_time": "2022-11-03 08:00:00",
|
||||
"location": "",
|
||||
"description": "Take control of the most technologically advanced army in the Imperium - The Adeptus Mechanicus. Your every decision will weigh heavily on the outcome of the mission, in this turn-based tactical game. Will you be blessed by the Omnissiah?\n\nhttps://store.epicgames.com/fr/p/warhammer-mechanicus-0e4b71",
|
||||
}
|
||||
|
||||
|
||||
async def test_attribute_not_found(
|
||||
hass: HomeAssistant,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
service_attribute_not_found: Mock,
|
||||
) -> None:
|
||||
"""Test setup calendars with attribute not found error."""
|
||||
freezer.move_to("2023-10-12T00:00:00.000Z")
|
||||
|
||||
await setup_platform(hass, CALENDAR_DOMAIN)
|
||||
|
||||
state = hass.states.get("calendar.epic_games_store_discount_games")
|
||||
assert state.name == "Epic Games Store Discount games"
|
||||
state = hass.states.get("calendar.epic_games_store_free_games")
|
||||
assert state.name == "Epic Games Store Free games"
|
||||
assert state.state == STATE_ON
|
||||
|
||||
|
||||
async def test_christmas_special(
|
||||
hass: HomeAssistant,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
service_christmas_special: Mock,
|
||||
) -> None:
|
||||
"""Test setup calendars with Christmas special case."""
|
||||
freezer.move_to("2023-12-28T00:00:00.000Z")
|
||||
|
||||
await setup_platform(hass, CALENDAR_DOMAIN)
|
||||
|
||||
state = hass.states.get("calendar.epic_games_store_discount_games")
|
||||
assert state.name == "Epic Games Store Discount games"
|
||||
assert state.state == STATE_OFF
|
||||
|
||||
state = hass.states.get("calendar.epic_games_store_free_games")
|
||||
assert state.name == "Epic Games Store Free games"
|
||||
assert state.state == STATE_ON
|
||||
|
||||
|
||||
async def test_get_events(
|
||||
hass: HomeAssistant,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
service_multiple: Mock,
|
||||
) -> None:
|
||||
"""Test setup component with calendars."""
|
||||
freezer.move_to("2022-10-30T00:00:00.000Z")
|
||||
|
||||
await setup_platform(hass, CALENDAR_DOMAIN)
|
||||
|
||||
# 1 week in range of data
|
||||
result = await hass.services.async_call(
|
||||
CALENDAR_DOMAIN,
|
||||
SERVICE_GET_EVENTS,
|
||||
{
|
||||
ATTR_ENTITY_ID: ["calendar.epic_games_store_discount_games"],
|
||||
EVENT_START_DATETIME: dt_util.parse_datetime("2022-10-20T00:00:00.000Z"),
|
||||
EVENT_END_DATETIME: dt_util.parse_datetime("2022-10-27T00:00:00.000Z"),
|
||||
},
|
||||
blocking=True,
|
||||
return_response=True,
|
||||
)
|
||||
|
||||
assert len(result["calendar.epic_games_store_discount_games"]["events"]) == 3
|
||||
|
||||
# 1 week out of range of data
|
||||
result = await hass.services.async_call(
|
||||
CALENDAR_DOMAIN,
|
||||
SERVICE_GET_EVENTS,
|
||||
{
|
||||
ATTR_ENTITY_ID: ["calendar.epic_games_store_discount_games"],
|
||||
EVENT_START_DATETIME: dt_util.parse_datetime("1970-01-01T00:00:00.000Z"),
|
||||
EVENT_END_DATETIME: dt_util.parse_datetime("1970-01-08T00:00:00.000Z"),
|
||||
},
|
||||
blocking=True,
|
||||
return_response=True,
|
||||
)
|
||||
|
||||
assert len(result["calendar.epic_games_store_discount_games"]["events"]) == 0
|
142
tests/components/epic_games_store/test_config_flow.py
Normal file
142
tests/components/epic_games_store/test_config_flow.py
Normal file
@ -0,0 +1,142 @@
|
||||
"""Test the Epic Games Store config flow."""
|
||||
|
||||
from http.client import HTTPException
|
||||
from unittest.mock import patch
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.components.epic_games_store.config_flow import get_default_language
|
||||
from homeassistant.components.epic_games_store.const import DOMAIN
|
||||
from homeassistant.const import CONF_COUNTRY, CONF_LANGUAGE
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.data_entry_flow import FlowResultType
|
||||
|
||||
from .const import (
|
||||
DATA_ERROR_ATTRIBUTE_NOT_FOUND,
|
||||
DATA_ERROR_WRONG_COUNTRY,
|
||||
DATA_FREE_GAMES,
|
||||
MOCK_COUNTRY,
|
||||
MOCK_LANGUAGE,
|
||||
)
|
||||
|
||||
|
||||
async def test_default_language(hass: HomeAssistant) -> None:
|
||||
"""Test we get the form."""
|
||||
hass.config.language = "fr"
|
||||
hass.config.country = "FR"
|
||||
assert get_default_language(hass) == "fr"
|
||||
|
||||
hass.config.language = "es"
|
||||
hass.config.country = "ES"
|
||||
assert get_default_language(hass) == "es-ES"
|
||||
|
||||
hass.config.language = "en"
|
||||
hass.config.country = "AZ"
|
||||
assert get_default_language(hass) is None
|
||||
|
||||
|
||||
async def test_form(hass: HomeAssistant) -> None:
|
||||
"""Test we get the form."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
assert result["type"] == FlowResultType.FORM
|
||||
assert result["errors"] is None
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.epic_games_store.config_flow.EpicGamesStoreAPI.get_free_games",
|
||||
return_value=DATA_FREE_GAMES,
|
||||
):
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
CONF_LANGUAGE: MOCK_LANGUAGE,
|
||||
CONF_COUNTRY: MOCK_COUNTRY,
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result2["type"] == FlowResultType.CREATE_ENTRY
|
||||
assert result2["result"].unique_id == f"freegames-{MOCK_LANGUAGE}-{MOCK_COUNTRY}"
|
||||
assert (
|
||||
result2["title"]
|
||||
== f"Epic Games Store - Free Games ({MOCK_LANGUAGE}-{MOCK_COUNTRY})"
|
||||
)
|
||||
assert result2["data"] == {
|
||||
CONF_LANGUAGE: MOCK_LANGUAGE,
|
||||
CONF_COUNTRY: MOCK_COUNTRY,
|
||||
}
|
||||
|
||||
|
||||
async def test_form_cannot_connect(hass: HomeAssistant) -> None:
|
||||
"""Test we handle cannot connect error."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.epic_games_store.config_flow.EpicGamesStoreAPI.get_free_games",
|
||||
side_effect=HTTPException,
|
||||
):
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
CONF_LANGUAGE: MOCK_LANGUAGE,
|
||||
CONF_COUNTRY: MOCK_COUNTRY,
|
||||
},
|
||||
)
|
||||
|
||||
assert result2["type"] == FlowResultType.FORM
|
||||
assert result2["errors"] == {"base": "unknown"}
|
||||
|
||||
|
||||
async def test_form_cannot_connect_wrong_param(hass: HomeAssistant) -> None:
|
||||
"""Test we handle cannot connect error."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.epic_games_store.config_flow.EpicGamesStoreAPI.get_free_games",
|
||||
return_value=DATA_ERROR_WRONG_COUNTRY,
|
||||
):
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
CONF_LANGUAGE: MOCK_LANGUAGE,
|
||||
CONF_COUNTRY: MOCK_COUNTRY,
|
||||
},
|
||||
)
|
||||
|
||||
assert result2["type"] == FlowResultType.FORM
|
||||
assert result2["errors"] == {"base": "unknown"}
|
||||
|
||||
|
||||
async def test_form_service_error(hass: HomeAssistant) -> None:
|
||||
"""Test we handle service error gracefully."""
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||
)
|
||||
|
||||
with patch(
|
||||
"homeassistant.components.epic_games_store.config_flow.EpicGamesStoreAPI.get_free_games",
|
||||
return_value=DATA_ERROR_ATTRIBUTE_NOT_FOUND,
|
||||
):
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
CONF_LANGUAGE: MOCK_LANGUAGE,
|
||||
CONF_COUNTRY: MOCK_COUNTRY,
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result2["type"] == FlowResultType.CREATE_ENTRY
|
||||
assert result2["result"].unique_id == f"freegames-{MOCK_LANGUAGE}-{MOCK_COUNTRY}"
|
||||
assert (
|
||||
result2["title"]
|
||||
== f"Epic Games Store - Free Games ({MOCK_LANGUAGE}-{MOCK_COUNTRY})"
|
||||
)
|
||||
assert result2["data"] == {
|
||||
CONF_LANGUAGE: MOCK_LANGUAGE,
|
||||
CONF_COUNTRY: MOCK_COUNTRY,
|
||||
}
|
74
tests/components/epic_games_store/test_helper.py
Normal file
74
tests/components/epic_games_store/test_helper.py
Normal file
@ -0,0 +1,74 @@
|
||||
"""Tests for the Epic Games Store helpers."""
|
||||
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.epic_games_store.helper import (
|
||||
format_game_data,
|
||||
get_game_url,
|
||||
is_free_game,
|
||||
)
|
||||
|
||||
from .const import DATA_ERROR_ATTRIBUTE_NOT_FOUND, DATA_FREE_GAMES_ONE
|
||||
|
||||
FREE_GAMES_API = DATA_FREE_GAMES_ONE["data"]["Catalog"]["searchStore"]["elements"]
|
||||
FREE_GAME = FREE_GAMES_API[2]
|
||||
NOT_FREE_GAME = FREE_GAMES_API[0]
|
||||
|
||||
|
||||
def test_format_game_data() -> None:
|
||||
"""Test game data format."""
|
||||
game_data = format_game_data(FREE_GAME, "fr")
|
||||
assert game_data
|
||||
assert game_data["title"]
|
||||
assert game_data["description"]
|
||||
assert game_data["released_at"]
|
||||
assert game_data["original_price"]
|
||||
assert game_data["publisher"]
|
||||
assert game_data["url"]
|
||||
assert game_data["img_portrait"]
|
||||
assert game_data["img_landscape"]
|
||||
assert game_data["discount_type"] == "free"
|
||||
assert game_data["discount_start_at"]
|
||||
assert game_data["discount_end_at"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("raw_game_data", "expected_result"),
|
||||
[
|
||||
(
|
||||
DATA_ERROR_ATTRIBUTE_NOT_FOUND["data"]["Catalog"]["searchStore"][
|
||||
"elements"
|
||||
][1],
|
||||
"/p/destiny-2--bungie-30th-anniversary-pack",
|
||||
),
|
||||
(
|
||||
DATA_ERROR_ATTRIBUTE_NOT_FOUND["data"]["Catalog"]["searchStore"][
|
||||
"elements"
|
||||
][4],
|
||||
"/bundles/qube-ultimate-bundle",
|
||||
),
|
||||
(
|
||||
DATA_ERROR_ATTRIBUTE_NOT_FOUND["data"]["Catalog"]["searchStore"][
|
||||
"elements"
|
||||
][5],
|
||||
"/p/mystery-game-7",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_get_game_url(raw_game_data: dict[str, Any], expected_result: bool) -> None:
|
||||
"""Test to get the game URL."""
|
||||
assert get_game_url(raw_game_data, "fr").endswith(expected_result)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("raw_game_data", "expected_result"),
|
||||
[
|
||||
(FREE_GAME, True),
|
||||
(NOT_FREE_GAME, False),
|
||||
],
|
||||
)
|
||||
def test_is_free_game(raw_game_data: dict[str, Any], expected_result: bool) -> None:
|
||||
"""Test if this game is free."""
|
||||
assert is_free_game(raw_game_data) == expected_result
|
Loading…
x
Reference in New Issue
Block a user