Home Assistant Unofficial Reference 2024.12.1
config_flow.py
Go to the documentation of this file.
1 """Config Flow for PlayStation 4."""
2 
3 from collections import OrderedDict
4 from typing import Any
5 
6 from pyps4_2ndscreen.errors import CredentialTimeout
7 from pyps4_2ndscreen.helpers import Helper
8 from pyps4_2ndscreen.media_art import COUNTRIES
9 import voluptuous as vol
10 
11 from homeassistant.config_entries import ConfigFlow, ConfigFlowResult
12 from homeassistant.const import (
13  CONF_CODE,
14  CONF_HOST,
15  CONF_IP_ADDRESS,
16  CONF_NAME,
17  CONF_REGION,
18  CONF_TOKEN,
19 )
20 from homeassistant.helpers.aiohttp_client import async_get_clientsession
21 from homeassistant.util import location
22 
23 from .const import (
24  CONFIG_ENTRY_VERSION,
25  COUNTRYCODE_NAMES,
26  DEFAULT_ALIAS,
27  DEFAULT_NAME,
28  DOMAIN,
29 )
30 
31 CONF_MODE = "Config Mode"
32 CONF_AUTO = "Auto Discover"
33 CONF_MANUAL = "Manual Entry"
34 
35 LOCAL_UDP_PORT = 1988
36 UDP_PORT = 987
37 TCP_PORT = 997
38 PORT_MSG = {UDP_PORT: "port_987_bind_error", TCP_PORT: "port_997_bind_error"}
39 
40 PIN_LENGTH = 8
41 
42 
43 class PlayStation4FlowHandler(ConfigFlow, domain=DOMAIN):
44  """Handle a PlayStation 4 config flow."""
45 
46  VERSION = CONFIG_ENTRY_VERSION
47 
48  def __init__(self) -> None:
49  """Initialize the config flow."""
50  self.helperhelper = Helper()
51  self.credscreds: str | None = None
52  self.namename = None
53  self.hosthost = None
54  self.regionregion = None
55  self.pinpin: str | None = None
56  self.m_devicem_device = None
57  self.locationlocation: location.LocationInfo | None = None
58  self.device_listdevice_list: list[str] = []
59 
60  async def async_step_user(
61  self, user_input: dict[str, Any] | None = None
62  ) -> ConfigFlowResult:
63  """Handle a user config flow."""
64  # Check if able to bind to ports: UDP 987, TCP 997.
65  ports = PORT_MSG.keys()
66  failed = await self.hass.async_add_executor_job(self.helperhelper.port_bind, ports)
67  if failed in ports:
68  reason = PORT_MSG[failed]
69  return self.async_abortasync_abortasync_abort(reason=reason)
70  return await self.async_step_credsasync_step_creds()
71 
72  async def async_step_creds(
73  self, user_input: dict[str, Any] | None = None
74  ) -> ConfigFlowResult:
75  """Return PS4 credentials from 2nd Screen App."""
76  errors = {}
77  if user_input is not None:
78  try:
79  self.credscreds = await self.hass.async_add_executor_job(
80  self.helperhelper.get_creds, DEFAULT_ALIAS
81  )
82  if self.credscreds is not None:
83  return await self.async_step_modeasync_step_mode()
84  return self.async_abortasync_abortasync_abort(reason="credential_error")
85  except CredentialTimeout:
86  errors["base"] = "credential_timeout"
87 
88  return self.async_show_formasync_show_formasync_show_form(step_id="creds", errors=errors)
89 
90  async def async_step_mode(
91  self, user_input: dict[str, Any] | None = None
92  ) -> ConfigFlowResult:
93  """Prompt for mode."""
94  errors = {}
95  mode = [CONF_AUTO, CONF_MANUAL]
96 
97  if user_input is not None:
98  if user_input[CONF_MODE] == CONF_MANUAL:
99  try:
100  if device := user_input[CONF_IP_ADDRESS]:
101  self.m_devicem_device = device
102  except KeyError:
103  errors[CONF_IP_ADDRESS] = "no_ipaddress"
104  if not errors:
105  return await self.async_step_linkasync_step_link()
106 
107  mode_schema = OrderedDict[vol.Marker, Any]()
108  mode_schema[vol.Required(CONF_MODE, default=CONF_AUTO)] = vol.In(list(mode))
109  mode_schema[vol.Optional(CONF_IP_ADDRESS)] = str
110 
111  return self.async_show_formasync_show_formasync_show_form(
112  step_id="mode", data_schema=vol.Schema(mode_schema), errors=errors
113  )
114 
115  async def async_step_link(
116  self, user_input: dict[str, Any] | None = None
117  ) -> ConfigFlowResult:
118  """Prompt user input. Create or edit entry."""
119  regions = sorted(COUNTRIES.keys())
120  default_region = None
121  errors = {}
122 
123  if user_input is None:
124  # Search for device.
125  # If LOCAL_UDP_PORT cannot be used, a random port will be selected.
126  devices = await self.hass.async_add_executor_job(
127  self.helperhelper.has_devices, self.m_devicem_device, LOCAL_UDP_PORT
128  )
129 
130  # Abort if can't find device.
131  if not devices:
132  return self.async_abortasync_abortasync_abort(reason="no_devices_found")
133 
134  self.device_listdevice_list = [device["host-ip"] for device in devices]
135 
136  # Check that devices found aren't configured per account.
137  entries = self._async_current_entries_async_current_entries()
138  if entries:
139  # Retrieve device data from all entries if creds match.
140  conf_devices = [
141  device
142  for entry in entries
143  if self.credscreds == entry.data[CONF_TOKEN]
144  for device in entry.data["devices"]
145  ]
146 
147  # Remove configured device from search list.
148  for c_device in conf_devices:
149  if c_device["host"] in self.device_listdevice_list:
150  # Remove configured device from search list.
151  self.device_listdevice_list.remove(c_device["host"])
152 
153  # If list is empty then all devices are configured.
154  if not self.device_listdevice_list:
155  return self.async_abortasync_abortasync_abort(reason="already_configured")
156 
157  # Login to PS4 with user data.
158  if user_input is not None:
159  self.regionregion = user_input[CONF_REGION]
160  self.namename = user_input[CONF_NAME]
161  # Assume pin had leading zeros, before coercing to int.
162  self.pinpin = str(user_input[CONF_CODE]).zfill(PIN_LENGTH)
163  self.hosthost = user_input[CONF_IP_ADDRESS]
164 
165  is_ready, is_login = await self.hass.async_add_executor_job(
166  self.helperhelper.link,
167  self.hosthost,
168  self.credscreds,
169  self.pinpin,
170  DEFAULT_ALIAS,
171  LOCAL_UDP_PORT,
172  )
173 
174  if is_ready is False:
175  errors["base"] = "cannot_connect"
176  elif is_login is False:
177  errors["base"] = "login_failed"
178  else:
179  device = {
180  CONF_HOST: self.hosthost,
181  CONF_NAME: self.namename,
182  CONF_REGION: self.regionregion,
183  }
184 
185  # Create entry.
186  return self.async_create_entryasync_create_entryasync_create_entry(
187  title="PlayStation 4",
188  data={CONF_TOKEN: self.credscreds, "devices": [device]},
189  )
190 
191  # Try to find region automatically.
192  if not self.locationlocation:
193  self.locationlocation = await location.async_detect_location_info(
194  async_get_clientsession(self.hass)
195  )
196  if self.locationlocation:
197  country = COUNTRYCODE_NAMES.get(self.locationlocation.country_code)
198  if country in COUNTRIES:
199  default_region = country
200 
201  # Show User Input form.
202  link_schema = OrderedDict[vol.Marker, Any]()
203  link_schema[vol.Required(CONF_IP_ADDRESS)] = vol.In(list(self.device_listdevice_list))
204  link_schema[vol.Required(CONF_REGION, default=default_region)] = vol.In(
205  list(regions)
206  )
207  link_schema[vol.Required(CONF_CODE)] = vol.All(
208  vol.Strip, vol.Length(max=PIN_LENGTH), vol.Coerce(int)
209  )
210  link_schema[vol.Required(CONF_NAME, default=DEFAULT_NAME)] = str
211 
212  return self.async_show_formasync_show_formasync_show_form(
213  step_id="link", data_schema=vol.Schema(link_schema), errors=errors
214  )
ConfigFlowResult async_step_mode(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:92
ConfigFlowResult async_step_user(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:62
ConfigFlowResult async_step_creds(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:74
ConfigFlowResult async_step_link(self, dict[str, Any]|None user_input=None)
Definition: config_flow.py:117
ConfigFlowResult async_create_entry(self, *str title, Mapping[str, Any] data, str|None description=None, Mapping[str, str]|None description_placeholders=None, Mapping[str, Any]|None options=None)
list[ConfigEntry] _async_current_entries(self, bool|None include_ignore=None)
ConfigFlowResult async_abort(self, *str reason, Mapping[str, str]|None description_placeholders=None)
ConfigFlowResult async_show_form(self, *str|None step_id=None, vol.Schema|None data_schema=None, dict[str, str]|None errors=None, Mapping[str, str]|None description_placeholders=None, bool|None last_step=None, str|None preview=None)
str
_FlowResultT async_show_form(self, *str|None step_id=None, vol.Schema|None data_schema=None, dict[str, str]|None errors=None, Mapping[str, str]|None description_placeholders=None, bool|None last_step=None, str|None preview=None)
_FlowResultT async_create_entry(self, *str|None title=None, Mapping[str, Any] data, str|None description=None, Mapping[str, str]|None description_placeholders=None)
_FlowResultT async_abort(self, *str reason, Mapping[str, str]|None description_placeholders=None)
bool remove(self, _T matcher)
Definition: match.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)