11"""The pi_hole component."""
22
3+ import asyncio
34import logging
45from typing import Any , Literal
56
7+ import aiohttp
8+ from aiohttp import DummyCookieJar
69from hole import Hole , HoleV5 , HoleV6
710from hole .exceptions import HoleConnectionError , HoleError
811
1720)
1821from homeassistant .core import HomeAssistant , callback
1922from homeassistant .helpers import entity_registry as er
20- from homeassistant .helpers .aiohttp_client import async_get_clientsession
23+ from homeassistant .helpers .aiohttp_client import (
24+ async_create_clientsession ,
25+ async_get_clientsession ,
26+ )
2127
2228from .const import CONF_STATISTICS_ONLY , DOMAIN
2329from .coordinator import PiHoleConfigEntry , PiHoleData , PiHoleUpdateCoordinator
2430
2531_LOGGER = logging .getLogger (__name__ )
2632
33+ DATA_V6_CLIENTSESSIONS = f"{ DOMAIN } _v6_clientsessions"
34+
2735
2836PLATFORMS = [
2937 Platform .BINARY_SENSOR ,
@@ -110,7 +118,10 @@ def api_by_version(
110118
111119 if password is None :
112120 password = entry .get (CONF_API_KEY , "" )
113- session = async_get_clientsession (hass , entry [CONF_VERIFY_SSL ])
121+ if version == 6 :
122+ session = _async_get_v6_session (hass , entry [CONF_VERIFY_SSL ])
123+ else :
124+ session = async_get_clientsession (hass , entry [CONF_VERIFY_SSL ])
114125 hole_kwargs = {
115126 "host" : entry [CONF_HOST ],
116127 "session" : session ,
@@ -128,45 +139,72 @@ def api_by_version(
128139 return Hole (** hole_kwargs )
129140
130141
142+ @callback
143+ def _async_get_v6_session (
144+ hass : HomeAssistant , verify_ssl : bool
145+ ) -> aiohttp .ClientSession :
146+ """Get a session with an isolated cookie jar for the Pi-hole v6 API."""
147+ sessions : dict [bool , aiohttp .ClientSession ] = hass .data .setdefault (
148+ DATA_V6_CLIENTSESSIONS , {}
149+ )
150+ if verify_ssl not in sessions :
151+ sessions [verify_ssl ] = async_create_clientsession (
152+ hass , verify_ssl , cookie_jar = DummyCookieJar ()
153+ )
154+ return sessions [verify_ssl ]
155+
156+
157+ async def _async_is_v6_api (hass : HomeAssistant , entry : dict [str , Any ]) -> bool :
158+ """Check if the Pi-hole instance exposes the v6 API."""
159+ protocol = "https" if entry .get (CONF_SSL ) else "http"
160+ session = _async_get_v6_session (hass , entry [CONF_VERIFY_SSL ])
161+ url = f"{ protocol } ://{ entry [CONF_HOST ]} /api/info/version"
162+
163+ async with asyncio .timeout (5 ):
164+ async with session .get (url ) as response :
165+ try :
166+ data : Any = await response .json ()
167+ except aiohttp .ContentTypeError , ValueError :
168+ return False
169+
170+ if not isinstance (data , dict ):
171+ return False
172+
173+ if response .status == 200 :
174+ return isinstance (data .get ("version" ), dict )
175+
176+ if response .status == 401 :
177+ error = data .get ("error" )
178+ return isinstance (error , dict ) and error .get ("key" ) == "unauthorized"
179+
180+ return False
181+
182+
131183async def determine_api_version (
132184 hass : HomeAssistant , entry : dict [str , Any ]
133185) -> Literal [5 , 6 ]:
134186 """Determine the API version of the Pi-hole instance without requiring authentication.
135187
136- Neither API v5 or v6 provides an endpoint to check the version without authentication.
137- Version 6 provides other enddpoints that do not require authentication , so we can use those to determine the version
138- version 5 returns an empty list in response to unauthenticated requests.
188+ Version 6 returns either version data or a distinct unauthorized error from
189+ /api/info/version , so we can use that endpoint to determine the version.
190+ Version 5 returns an empty list in response to unauthenticated requests.
139191 Because we are using endpoints that are not designed for this purpose, we should log liberally to help with debugging.
140192 """
141193
142- holeV6 = api_by_version (hass , entry , 6 , password = "wrong_password" )
143194 try :
144- await holeV6 .authenticate ()
145- except HoleConnectionError as err :
146- _LOGGER .error (
147- "Unexpected error connecting to Pi-hole v6 API at %s: %s. Trying version 5 API" ,
148- holeV6 .base_url ,
149- err ,
150- )
151- # Ideally python-hole would raise a specific exception for authentication failures
152- except HoleError as ex_v6 :
153- if str (ex_v6 ) == "Authentication failed: Invalid password" :
195+ if await _async_is_v6_api (hass , entry ):
154196 _LOGGER .debug (
155- "Success connecting to Pi-hole at %s without auth, API version is : %s" ,
156- holeV6 .base_url ,
157- 6 ,
197+ "Response from v6 API without auth, Pi-hole API version 6 probably detected at %s" ,
198+ entry [CONF_HOST ],
158199 )
159200 return 6
160- _LOGGER .debug (
161- "Connection to %s failed: %s, trying API version 5" , holeV6 .base_url , ex_v6
162- )
163- else :
164- # It seems that occasionally the auth can succeed unexpectedly when there is a valid session
165- _LOGGER .warning (
166- "Authenticated with %s through v6 API, but succeeded with an incorrect password. This is a known bug" ,
167- holeV6 .base_url ,
201+ except (TimeoutError , aiohttp .ClientError ) as err :
202+ _LOGGER .error (
203+ "Unexpected error connecting to Pi-hole v6 API at %s: %s. Trying version 5 API" ,
204+ entry [CONF_HOST ],
205+ err ,
168206 )
169- return 6
207+
170208 holeV5 = api_by_version (hass , entry , 5 , password = "wrong_token" )
171209 try :
172210 await holeV5 .get_data ()
@@ -190,6 +228,6 @@ async def determine_api_version(
190228 )
191229 _LOGGER .debug (
192230 "Could not determine pi-hole API version at: %s" ,
193- holeV6 . base_url ,
231+ entry [ CONF_HOST ] ,
194232 )
195233 raise HoleError ("Could not determine Pi-hole API version" )
0 commit comments