Home Assistant Unofficial Reference 2024.12.1
__init__.py
Go to the documentation of this file.
1 """Support for PlayStation 4 consoles."""
2 
3 import logging
4 import os
5 
6 from pyps4_2ndscreen.ddp import async_create_ddp_endpoint
7 from pyps4_2ndscreen.media_art import COUNTRIES
8 import voluptuous as vol
9 
10 from homeassistant.components import persistent_notification
12  ATTR_MEDIA_CONTENT_TYPE,
13  ATTR_MEDIA_TITLE,
14  MediaType,
15 )
16 from homeassistant.config_entries import ConfigEntry
17 from homeassistant.const import (
18  ATTR_COMMAND,
19  ATTR_ENTITY_ID,
20  ATTR_LOCKED,
21  CONF_REGION,
22  CONF_TOKEN,
23  Platform,
24 )
25 from homeassistant.core import HomeAssistant, ServiceCall, split_entity_id
26 from homeassistant.exceptions import HomeAssistantError
27 from homeassistant.helpers import config_validation as cv, entity_registry as er
28 from homeassistant.helpers.aiohttp_client import async_get_clientsession
29 from homeassistant.helpers.json import save_json
30 from homeassistant.helpers.typing import ConfigType
31 from homeassistant.util import location
32 from homeassistant.util.json import JsonObjectType, load_json_object
33 
34 from .config_flow import PlayStation4FlowHandler # noqa: F401
35 from .const import (
36  ATTR_MEDIA_IMAGE_URL,
37  COMMANDS,
38  COUNTRYCODE_NAMES,
39  DOMAIN,
40  GAMES_FILE,
41  PS4_DATA,
42 )
43 
44 _LOGGER = logging.getLogger(__name__)
45 
46 SERVICE_COMMAND = "send_command"
47 
48 PS4_COMMAND_SCHEMA = vol.Schema(
49  {
50  vol.Required(ATTR_ENTITY_ID): cv.entity_ids,
51  vol.Required(ATTR_COMMAND): vol.In(list(COMMANDS)),
52  }
53 )
54 
55 PLATFORMS = [Platform.MEDIA_PLAYER]
56 
57 CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
58 
59 
60 class PS4Data:
61  """Init Data Class."""
62 
63  def __init__(self):
64  """Init Class."""
65  self.devicesdevices = []
66  self.protocolprotocol = None
67 
68 
69 async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
70  """Set up the PS4 Component."""
71  hass.data[PS4_DATA] = PS4Data()
72 
73  transport, protocol = await async_create_ddp_endpoint()
74  hass.data[PS4_DATA].protocol = protocol
75  _LOGGER.debug("PS4 DDP endpoint created: %s, %s", transport, protocol)
76  service_handle(hass)
77  return True
78 
79 
80 async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
81  """Set up PS4 from a config entry."""
82  await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
83  return True
84 
85 
86 async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
87  """Unload a PS4 config entry."""
88  return await hass.config_entries.async_unload_platforms(entry, PLATFORMS)
89 
90 
91 async def async_migrate_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
92  """Migrate old entry."""
93  config_entries = hass.config_entries
94  data = entry.data
95  version = entry.version
96 
97  _LOGGER.debug("Migrating PS4 entry from Version %s", version)
98 
99  reason = {
100  1: "Region codes have changed",
101  2: "Format for Unique ID for entity registry has changed",
102  }
103 
104  # Migrate Version 1 -> Version 2: New region codes.
105  if version == 1:
106  loc = await location.async_detect_location_info(async_get_clientsession(hass))
107  if loc:
108  country = COUNTRYCODE_NAMES.get(loc.country_code)
109  if country in COUNTRIES:
110  for device in data["devices"]:
111  device[CONF_REGION] = country
112  version = 2
113  config_entries.async_update_entry(entry, data=data, version=2)
114  _LOGGER.debug(
115  "PlayStation 4 Config Updated: Region changed to: %s",
116  country,
117  )
118 
119  # Migrate Version 2 -> Version 3: Update identifier format.
120  if version == 2:
121  # Prevent changing entity_id. Updates entity registry.
122  registry = er.async_get(hass)
123 
124  for e_entry in registry.entities.get_entries_for_config_entry_id(
125  entry.entry_id
126  ):
127  unique_id = e_entry.unique_id
128  entity_id = e_entry.entity_id
129 
130  # Remove old entity entry.
131  registry.async_remove(entity_id)
132 
133  # Format old unique_id.
134  unique_id = format_unique_id(entry.data[CONF_TOKEN], unique_id)
135 
136  # Create new entry with old entity_id.
137  new_id = split_entity_id(entity_id)[1]
138  registry.async_get_or_create(
139  "media_player",
140  DOMAIN,
141  unique_id,
142  suggested_object_id=new_id,
143  config_entry=entry,
144  device_id=e_entry.device_id,
145  )
146  _LOGGER.debug(
147  "PlayStation 4 identifier for entity: %s has changed",
148  entity_id,
149  )
150  config_entries.async_update_entry(entry, version=3)
151  return True
152 
153  msg = f"""{reason[version]} for the PlayStation 4 Integration.
154  Please remove the PS4 Integration and re-configure
155  [here](/config/integrations)."""
156 
157  persistent_notification.async_create(
158  hass,
159  title="PlayStation 4 Integration Configuration Requires Update",
160  message=msg,
161  notification_id="config_entry_migration",
162  )
163  return False
164 
165 
166 def format_unique_id(creds, mac_address):
167  """Use last 4 Chars of credential as suffix. Unique ID per PSN user."""
168  suffix = creds[-4:]
169  return f"{mac_address}_{suffix}"
170 
171 
172 def load_games(hass: HomeAssistant, unique_id: str) -> JsonObjectType:
173  """Load games for sources."""
174  g_file = hass.config.path(GAMES_FILE.format(unique_id))
175  try:
176  games = load_json_object(g_file)
177  except HomeAssistantError as error:
178  games = {}
179  _LOGGER.error("Failed to load games file: %s", error)
180 
181  # If file exists
182  if os.path.isfile(g_file):
183  games = _reformat_data(hass, games, unique_id)
184  return games
185 
186 
187 def save_games(hass: HomeAssistant, games: dict, unique_id: str):
188  """Save games to file."""
189  g_file = hass.config.path(GAMES_FILE.format(unique_id))
190  try:
191  save_json(g_file, games)
192  except OSError as error:
193  _LOGGER.error("Could not save game list, %s", error)
194 
195 
196 def _reformat_data(hass: HomeAssistant, games: dict, unique_id: str) -> dict:
197  """Reformat data to correct format."""
198  data_reformatted = False
199 
200  for game, data in games.items():
201  # Convert str format to dict format.
202  if not isinstance(data, dict):
203  # Use existing title. Assign defaults.
204  games[game] = {
205  ATTR_LOCKED: False,
206  ATTR_MEDIA_TITLE: data,
207  ATTR_MEDIA_IMAGE_URL: None,
208  ATTR_MEDIA_CONTENT_TYPE: MediaType.GAME,
209  }
210  data_reformatted = True
211 
212  _LOGGER.debug("Reformatting media data for item: %s, %s", game, data)
213 
214  if data_reformatted:
215  save_games(hass, games, unique_id)
216  return games
217 
218 
219 def service_handle(hass: HomeAssistant):
220  """Handle for services."""
221 
222  async def async_service_command(call: ServiceCall) -> None:
223  """Service for sending commands."""
224  entity_ids = call.data[ATTR_ENTITY_ID]
225  command = call.data[ATTR_COMMAND]
226  for device in hass.data[PS4_DATA].devices:
227  if device.entity_id in entity_ids:
228  await device.async_send_command(command)
229 
230  hass.services.async_register(
231  DOMAIN, SERVICE_COMMAND, async_service_command, schema=PS4_COMMAND_SCHEMA
232  )
bool async_migrate_entry(HomeAssistant hass, ConfigEntry entry)
Definition: __init__.py:91
bool async_setup(HomeAssistant hass, ConfigType config)
Definition: __init__.py:69
bool async_setup_entry(HomeAssistant hass, ConfigEntry entry)
Definition: __init__.py:80
dict _reformat_data(HomeAssistant hass, dict games, str unique_id)
Definition: __init__.py:196
JsonObjectType load_games(HomeAssistant hass, str unique_id)
Definition: __init__.py:172
bool async_unload_entry(HomeAssistant hass, ConfigEntry entry)
Definition: __init__.py:86
def format_unique_id(creds, mac_address)
Definition: __init__.py:166
def save_games(HomeAssistant hass, dict games, str unique_id)
Definition: __init__.py:187
def service_handle(HomeAssistant hass)
Definition: __init__.py:219
tuple[str, str] split_entity_id(str entity_id)
Definition: core.py:214
aiohttp.ClientSession async_get_clientsession(HomeAssistant hass, bool verify_ssl=True, socket.AddressFamily family=socket.AF_UNSPEC, ssl_util.SSLCipherList ssl_cipher=ssl_util.SSLCipherList.PYTHON_DEFAULT)
None save_json(str filename, list|dict data, bool private=False, *type[json.JSONEncoder]|None encoder=None, bool atomic_writes=False)
Definition: json.py:202
JsonObjectType load_json_object(str|PathLike[str] filename, JsonObjectType default=_SENTINEL)
Definition: json.py:109