initial commit
This commit is contained in:
commit
f8971c4821
18 files changed
+1067
No files matched your search
@@ -0,0 +1 @@
|
||||
/div/
|
||||
Generated
+10
@@ -0,0 +1,10 @@
|
||||
# Default ignored files
|
||||
/shelf/
|
||||
/workspace.xml
|
||||
# Ignored default folder with query files
|
||||
/queries/
|
||||
# Datasource local storage ignored files
|
||||
/dataSources/
|
||||
/dataSources.local.xml
|
||||
# Editor-based HTTP Client requests
|
||||
/httpRequests/
|
||||
Generated
+10
@@ -0,0 +1,10 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<module type="PYTHON_MODULE" version="4">
|
||||
<component name="NewModuleRootManager">
|
||||
<content url="file://$MODULE_DIR$">
|
||||
<excludeFolder url="file://$MODULE_DIR$/.venv" />
|
||||
</content>
|
||||
<orderEntry type="jdk" jdkName="Python 3.14 (bravia8A)" jdkType="Python SDK" />
|
||||
<orderEntry type="sourceFolder" forTests="false" />
|
||||
</component>
|
||||
</module>
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
<component name="InspectionProjectProfileManager">
|
||||
<settings>
|
||||
<option name="USE_PROJECT_PROFILE" value="false" />
|
||||
<version value="1.0" />
|
||||
</settings>
|
||||
</component>
|
||||
Generated
+7
@@ -0,0 +1,7 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="Black">
|
||||
<option name="sdkName" value="Python 3.14 (bravia8A)" />
|
||||
</component>
|
||||
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.14 (bravia8A)" project-jdk-type="Python SDK" />
|
||||
</project>
|
||||
Generated
+8
@@ -0,0 +1,8 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="ProjectModuleManager">
|
||||
<modules>
|
||||
<module fileurl="file://$PROJECT_DIR$/.idea/bravia8A.iml" filepath="$PROJECT_DIR$/.idea/bravia8A.iml" />
|
||||
</modules>
|
||||
</component>
|
||||
</project>
|
||||
Generated
+6
@@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project version="4">
|
||||
<component name="VcsDirectoryMappings">
|
||||
<mapping directory="$PROJECT_DIR$" vcs="Git" />
|
||||
</component>
|
||||
</project>
|
||||
@@ -0,0 +1,15 @@
|
||||
# Control Sony BRAVIA 8A via HTTPS in Home Assistant
|
||||
|
||||
The BRAVIA 8A uses only HTTPS for communication, which makes the Home Assistant integration SonyBravia unusable. This project provides a workaround.
|
||||
|
||||
We get the directory ```pybravia``` from https://github.com/Drafteed/pybravia rename it to ```pybravia8``` and modify it to use HTTPS (changes in ```client.py```). The Module ```bravia8``` uses this.
|
||||
|
||||
The main script ```bravia8_pyscript.py``` utilizes the HACS custom integration ```pyscript``` (https://github.com/custom-components/pyscript).
|
||||
|
||||
## Installation
|
||||
|
||||
Install ```pyscript``` following the instructions on the [pyscript GitHub repository](https://github.com/custom-components/pyscript).
|
||||
|
||||
Copy ```bravia8/``` and ```pybravia8/``` to ```<HA-config>/pyscript/modules/```.
|
||||
|
||||
Copy ```bravia8_pyscript.py``` to ```<HA-config>/pyscript/```.
|
||||
@@ -0,0 +1,2 @@
|
||||
|
||||
from .remote import BraviaRemote
|
||||
@@ -0,0 +1,90 @@
|
||||
import logging
|
||||
|
||||
from pybravia8 import BraviaClient, BraviaError
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
class BraviaRemote:
|
||||
def __init__(self, host, psk):
|
||||
self.client = BraviaClient(host)
|
||||
self.psk = psk
|
||||
self.connected = False
|
||||
|
||||
async def connect(self):
|
||||
if not self.connected:
|
||||
try:
|
||||
await self.client.connect(psk=self.psk)
|
||||
self.connected = True
|
||||
except BraviaError as e:
|
||||
_LOGGER.error(f"Error connecting to Bravia TV: {e}")
|
||||
|
||||
async def disconnect(self):
|
||||
await self.client.disconnect()
|
||||
self.connected = False
|
||||
|
||||
async def send_command(self, cmd):
|
||||
await self.connect()
|
||||
try:
|
||||
_LOGGER.info(f"Sending {cmd}")
|
||||
rc = await self.client.send_command(cmd)
|
||||
_LOGGER.info(f"send command '{cmd}' returned: {rc}")
|
||||
except BraviaError as e:
|
||||
_LOGGER.error(f"Error sending command '{cmd}': {e}")
|
||||
|
||||
async def turn_on(self):
|
||||
await self.disconnect()
|
||||
await self.connect()
|
||||
try:
|
||||
_LOGGER.info(f"Turning TV on")
|
||||
rc = await self.client.turn_on()
|
||||
_LOGGER.info(f"turn_on() returned: {rc}")
|
||||
except BraviaError as e:
|
||||
_LOGGER.error(f"Error turning TV on: {e}")
|
||||
|
||||
async def turn_off(self):
|
||||
await self.connect()
|
||||
try:
|
||||
_LOGGER.info(f"Turning TV off")
|
||||
rc = await self.client.turn_off()
|
||||
_LOGGER.info(f"turn_off() returned: {rc}")
|
||||
await self.disconnect()
|
||||
except BraviaError as e:
|
||||
_LOGGER.error(f"Error turning TV off: {e}")
|
||||
|
||||
async def volume_up(self):
|
||||
await self.connect()
|
||||
try:
|
||||
_LOGGER.info(f"Increasing volume")
|
||||
rc = await self.client.volume_up()
|
||||
_LOGGER.info(f"volume_up() returned: {rc}")
|
||||
except BraviaError as e:
|
||||
_LOGGER.error(f"Error increasing volume: {e}")
|
||||
|
||||
async def volume_down(self):
|
||||
await self.connect()
|
||||
try:
|
||||
_LOGGER.info(f"Decreasing volume")
|
||||
rc = await self.client.volume_down()
|
||||
_LOGGER.info(f"volume_down() returned: {rc}")
|
||||
except BraviaError as e:
|
||||
_LOGGER.error(f"Error decreasing volume: {e}")
|
||||
|
||||
async def mute(self, mute: bool = None):
|
||||
await self.connect()
|
||||
try:
|
||||
_LOGGER.info(f"Toggling mute")
|
||||
rc = await self.client.volume_mute(mute)
|
||||
_LOGGER.info(f"mute() returned: {rc}")
|
||||
except BraviaError as e:
|
||||
_LOGGER.error(f"Error toggling mute: {e}")
|
||||
|
||||
async def dump_commands(self):
|
||||
await self.connect()
|
||||
cmd_list = await self.client.get_command_list()
|
||||
commands = ", ".join(cmd_list.keys())
|
||||
_LOGGER.info(f"Available commands: \n{commands}")
|
||||
|
||||
async def dump_info(self):
|
||||
await self.connect()
|
||||
info = await self.client.get_system_info()
|
||||
_LOGGER.info(f"System info: \n{info}")
|
||||
@@ -0,0 +1,51 @@
|
||||
import logging
|
||||
|
||||
from bravia8 import BraviaRemote
|
||||
|
||||
logging.basicConfig(level=logging.DEBUG)
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
HOST = "10.4.2.60"
|
||||
PSK = "12345678901234567890"
|
||||
|
||||
remote = BraviaRemote(host=HOST, psk=PSK)
|
||||
|
||||
@service
|
||||
def bravia_send_command(command=None):
|
||||
remote.send_command(command)
|
||||
|
||||
@service
|
||||
def bravia_turn_on():
|
||||
remote.turn_on()
|
||||
|
||||
@service
|
||||
def bravia_turn_off():
|
||||
remote.turn_off()
|
||||
|
||||
@service
|
||||
def bravia_volume_up():
|
||||
remote.volume_up()
|
||||
|
||||
@service
|
||||
def bravia_volume_down():
|
||||
remote.volume_down()
|
||||
|
||||
@service
|
||||
def bravia_mute():
|
||||
remote.mute()
|
||||
|
||||
@service
|
||||
def bravia_mute_on():
|
||||
remote.mute(True)
|
||||
|
||||
@service
|
||||
def bravia_mute_off():
|
||||
remote.mute(False)
|
||||
|
||||
@service
|
||||
def bravia_dump_commands():
|
||||
remote.dump_commands()
|
||||
|
||||
@service
|
||||
def bravia_dump_info():
|
||||
remote.dump_info()
|
||||
+147
@@ -0,0 +1,147 @@
|
||||
Num1: AAAAAQAAAAEAAAAAAw==
|
||||
Num2: AAAAAQAAAAEAAAABAw==
|
||||
Num3: AAAAAQAAAAEAAAACAw==
|
||||
Num4: AAAAAQAAAAEAAAADAw==
|
||||
Num5: AAAAAQAAAAEAAAAEAw==
|
||||
Num6: AAAAAQAAAAEAAAAFAw==
|
||||
Num7: AAAAAQAAAAEAAAAGAw==
|
||||
Num8: AAAAAQAAAAEAAAAHAw==
|
||||
Num9: AAAAAQAAAAEAAAAIAw==
|
||||
Num0: AAAAAQAAAAEAAAAJAw==
|
||||
Num11: AAAAAQAAAAEAAAAKAw==
|
||||
Num12: AAAAAQAAAAEAAAALAw==
|
||||
Enter: AAAAAQAAAAEAAAALAw==
|
||||
GGuide: AAAAAQAAAAEAAAAOAw==
|
||||
ChannelUp: AAAAAQAAAAEAAAAQAw==
|
||||
ChannelDown: AAAAAQAAAAEAAAARAw==
|
||||
VolumeUp: AAAAAQAAAAEAAAASAw==
|
||||
VolumeDown: AAAAAQAAAAEAAAATAw==
|
||||
Mute: AAAAAQAAAAEAAAAUAw==
|
||||
TvPower: AAAAAQAAAAEAAAAVAw==
|
||||
MediaAudioTrack: AAAAAQAAAAEAAAAXAw==
|
||||
Tv: AAAAAQAAAAEAAAAkAw==
|
||||
TvInput: AAAAAQAAAAEAAAAlAw==
|
||||
TvAntennaCable: AAAAAQAAAAEAAAAqAw==
|
||||
WakeUp: AAAAAQAAAAEAAAAuAw==
|
||||
Sleep: AAAAAQAAAAEAAAAvAw==
|
||||
Right: AAAAAQAAAAEAAAAzAw==
|
||||
Left: AAAAAQAAAAEAAAA0Aw==
|
||||
SleepTimer: AAAAAQAAAAEAAAA2Aw==
|
||||
TvAnalog: AAAAAQAAAAEAAAA4Aw==
|
||||
Display: AAAAAQAAAAEAAAA6Aw==
|
||||
Jump: AAAAAQAAAAEAAAA7Aw==
|
||||
PictureOff: AAAAAQAAAAEAAAA+Aw==
|
||||
Teletext: AAAAAQAAAAEAAAA/Aw==
|
||||
Video1: AAAAAQAAAAEAAABAAw==
|
||||
Video2: AAAAAQAAAAEAAABBAw==
|
||||
AnalogRgb1: AAAAAQAAAAEAAABDAw==
|
||||
Home: AAAAAQAAAAEAAABgAw==
|
||||
Exit: AAAAAQAAAAEAAABjAw==
|
||||
PictureMode: AAAAAQAAAAEAAABkAw==
|
||||
Confirm: AAAAAQAAAAEAAABlAw==
|
||||
Up: AAAAAQAAAAEAAAB0Aw==
|
||||
Down: AAAAAQAAAAEAAAB1Aw==
|
||||
*AD: AAAAAgAAABoAAAA7Aw==
|
||||
AudioMixUp: AAAAAgAAABoAAAA8Aw==
|
||||
AudioMixDown: AAAAAgAAABoAAAA9Aw==
|
||||
PhotoFrame: AAAAAgAAABoAAABVAw==
|
||||
Tv_Radio: AAAAAgAAABoAAABXAw==
|
||||
SyncMenu: AAAAAgAAABoAAABYAw==
|
||||
Hdmi1: AAAAAgAAABoAAABaAw==
|
||||
Hdmi2: AAAAAgAAABoAAABbAw==
|
||||
Hdmi3: AAAAAgAAABoAAABcAw==
|
||||
Hdmi4: AAAAAgAAABoAAABdAw==
|
||||
TopMenu: AAAAAgAAABoAAABgAw==
|
||||
PopUpMenu: AAAAAgAAABoAAABhAw==
|
||||
OneTouchTimeRec: AAAAAgAAABoAAABkAw==
|
||||
OneTouchView: AAAAAgAAABoAAABlAw==
|
||||
ControlMenu: AAAAAgAAABoAAAB1Aw==
|
||||
FootballMode: AAAAAgAAABoAAAB2Aw==
|
||||
iManual: AAAAAgAAABoAAAB7Aw==
|
||||
Netflix: AAAAAgAAABoAAAB8Aw==
|
||||
Analog: AAAAAgAAAHcAAAANAw==
|
||||
Mode3D: AAAAAgAAAHcAAABNAw==
|
||||
DigitalToggle: AAAAAgAAAHcAAABSAw==
|
||||
DemoSurround: AAAAAgAAAHcAAAB7Aw==
|
||||
TenKey: AAAAAgAAAJcAAAAMAw==
|
||||
PlayPause: AAAAAgAAAJcAAAAPAw==
|
||||
BSCS: AAAAAgAAAJcAAAAQAw==
|
||||
Ddata: AAAAAgAAAJcAAAAVAw==
|
||||
Stop: AAAAAgAAAJcAAAAYAw==
|
||||
Pause: AAAAAgAAAJcAAAAZAw==
|
||||
Play: AAAAAgAAAJcAAAAaAw==
|
||||
Rewind: AAAAAgAAAJcAAAAbAw==
|
||||
Forward: AAAAAgAAAJcAAAAcAw==
|
||||
DOT: AAAAAgAAAJcAAAAdAw==
|
||||
Rec: AAAAAgAAAJcAAAAgAw==
|
||||
Return: AAAAAgAAAJcAAAAjAw==
|
||||
Blue: AAAAAgAAAJcAAAAkAw==
|
||||
Red: AAAAAgAAAJcAAAAlAw==
|
||||
Green: AAAAAgAAAJcAAAAmAw==
|
||||
Yellow: AAAAAgAAAJcAAAAnAw==
|
||||
SubTitle: AAAAAgAAAJcAAAAoAw==
|
||||
CS: AAAAAgAAAJcAAAArAw==
|
||||
BS: AAAAAgAAAJcAAAAsAw==
|
||||
Digital: AAAAAgAAAJcAAAAyAw==
|
||||
Options: AAAAAgAAAJcAAAA2Aw==
|
||||
Media: AAAAAgAAAJcAAAA4Aw==
|
||||
Prev: AAAAAgAAAJcAAAA8Aw==
|
||||
Next: AAAAAgAAAJcAAAA9Aw==
|
||||
DpadCenter: AAAAAgAAAJcAAABKAw==
|
||||
CursorLeft: AAAAAgAAAJcAAABNAw==
|
||||
CursorRight: AAAAAgAAAJcAAABOAw==
|
||||
CursorUp: AAAAAgAAAJcAAABPAw==
|
||||
CursorDown: AAAAAgAAAJcAAABQAw==
|
||||
ShopRemoteControlForcedDynamic: AAAAAgAAAJcAAABqAw==
|
||||
FlashPlus: AAAAAgAAAJcAAAB4Aw==
|
||||
FlashMinus: AAAAAgAAAJcAAAB5Aw==
|
||||
DemoMode: AAAAAgAAAJcAAAB8Aw==
|
||||
ClosedCaption: AAAAAgAAAKQAAAAQAw==
|
||||
Component1: AAAAAgAAAKQAAAA2Aw==
|
||||
Component2: AAAAAgAAAKQAAAA3Aw==
|
||||
Wide: AAAAAgAAAKQAAAA9Aw==
|
||||
EPG: AAAAAgAAAKQAAABbAw==
|
||||
PAP: AAAAAgAAAKQAAAB3Aw==
|
||||
AdvancedBSCS: AAAAAgAAAMQAAAAMAw==
|
||||
AudioOutput_AudioSystem: AAAAAgAAAMQAAAAiAw==
|
||||
AudioOutput_Toggle: AAAAAgAAAMQAAAAkAw==
|
||||
DarwinLoop: AAAAAgAAAMQAAAAlAw==
|
||||
ApplicationLauncher: AAAAAgAAAMQAAAAqAw==
|
||||
AcousticCenterSync: AAAAAgAAAMQAAAArAw==
|
||||
Assists: AAAAAgAAAMQAAAA7Aw==
|
||||
FeaturedApp: AAAAAgAAAMQAAABEAw==
|
||||
FeaturedAppVOD: AAAAAgAAAMQAAABFAw==
|
||||
GooglePlay: AAAAAgAAAMQAAABGAw==
|
||||
YouTube: AAAAAgAAAMQAAABHAw==
|
||||
RecorderMenu: AAAAAgAAAMQAAABIAw==
|
||||
STBMenu: AAAAAgAAAMQAAABJAw==
|
||||
ActionMenu: AAAAAgAAAMQAAABLAw==
|
||||
Help: AAAAAgAAAMQAAABNAw==
|
||||
TvSatellite: AAAAAgAAAMQAAABOAw==
|
||||
AndroidMenu: AAAAAgAAAMQAAABPAw==
|
||||
MicPressedFromPairedRC: AAAAAgAAAMQAAABuAw==
|
||||
MicPressedFromUnpairedRC: AAAAAgAAAMQAAABvAw==
|
||||
GoogleDashboard: AAAAAgAAAMQAAABwAw==
|
||||
AutoParingRequest: AAAAAgAAAMQAAAB7Aw==
|
||||
B2bCustomizeKey: AAAAAgAAAMQAAAB8Aw==
|
||||
WirelessSubwoofer: AAAAAgAAAMQAAAB+Aw==
|
||||
PartnerApp1: AAAAAwAACB8AAAAAAw==
|
||||
PartnerApp2: AAAAAwAACB8AAAABAw==
|
||||
PartnerApp3: AAAAAwAACB8AAAACAw==
|
||||
PartnerApp4: AAAAAwAACB8AAAADAw==
|
||||
PartnerApp5: AAAAAwAACB8AAAAEAw==
|
||||
PartnerApp6: AAAAAwAACB8AAAAFAw==
|
||||
PartnerApp7: AAAAAwAACB8AAAAGAw==
|
||||
PartnerApp8: AAAAAwAACB8AAAAHAw==
|
||||
PartnerApp9: AAAAAwAACB8AAAAIAw==
|
||||
PartnerApp10: AAAAAwAACB8AAAAJAw==
|
||||
PartnerApp11: AAAAAwAACB8AAAAKAw==
|
||||
PartnerApp12: AAAAAwAACB8AAAALAw==
|
||||
PartnerApp13: AAAAAwAACB8AAAAMAw==
|
||||
PartnerApp14: AAAAAwAACB8AAAANAw==
|
||||
PartnerApp15: AAAAAwAACB8AAAAOAw==
|
||||
PartnerApp16: AAAAAwAACB8AAAAPAw==
|
||||
PartnerApp17: AAAAAwAACB8AAAAQAw==
|
||||
PartnerApp18: AAAAAwAACB8AAAARAw==
|
||||
PartnerApp19: AAAAAwAACB8AAAASAw==
|
||||
PartnerApp20: AAAAAwAACB8AAAATAw==
|
||||
@@ -0,0 +1,20 @@
|
||||
"""Python library for remote control of Sony Bravia TV."""
|
||||
|
||||
""" *hb*
|
||||
|
||||
Modified for for Sony Bravia 8A:
|
||||
Use https with self signed certificate -> client.py
|
||||
"""
|
||||
# flake8: noqa
|
||||
from .client import BraviaClient
|
||||
from .exceptions import (
|
||||
BraviaAuthError,
|
||||
BraviaConnectionError,
|
||||
BraviaConnectionTimeout,
|
||||
BraviaError,
|
||||
BraviaNotFound,
|
||||
BraviaNotSupported,
|
||||
BraviaTurnedOff,
|
||||
)
|
||||
|
||||
__version__ = "0.3.4.hb"
|
||||
@@ -0,0 +1,627 @@
|
||||
"""Python library for remote control of Sony Bravia TV."""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import socket
|
||||
from contextlib import suppress
|
||||
from datetime import datetime, timedelta
|
||||
from types import TracebackType
|
||||
from typing import Any
|
||||
|
||||
from aiohttp import BasicAuth, ClientError, ClientSession, CookieJar
|
||||
|
||||
from .const import (
|
||||
CODE_POWER_ON,
|
||||
DEFAULT_AUDIO_TARGET,
|
||||
DEFAULT_TIMEOUT,
|
||||
DEFAULT_VERSION,
|
||||
PAIR_PIN,
|
||||
SERVICE_ACCESS_CONTROL,
|
||||
SERVICE_APP_CONTROL,
|
||||
SERVICE_AUDIO,
|
||||
SERVICE_AV_CONTENT,
|
||||
SERVICE_GUIDE,
|
||||
SERVICE_IRCC,
|
||||
SERVICE_SYSTEM,
|
||||
SERVICE_VIDEO,
|
||||
)
|
||||
from .exceptions import (
|
||||
BraviaAuthError,
|
||||
BraviaConnectionError,
|
||||
BraviaConnectionTimeout,
|
||||
BraviaError,
|
||||
BraviaNotFound,
|
||||
BraviaNotSupported,
|
||||
BraviaTurnedOff,
|
||||
)
|
||||
from .util import normalize_cookies
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class BraviaClient:
|
||||
"""Represent a Bravia Client."""
|
||||
|
||||
def __init__(
|
||||
self, host: str, mac: str | None = None, session: ClientSession | None = None
|
||||
) -> None:
|
||||
"""Initialize the device."""
|
||||
self.host = host
|
||||
self.mac = mac
|
||||
self._session = session
|
||||
self._auth: BasicAuth | None = None
|
||||
self._psk: str | None = None
|
||||
self._ircc_time: datetime | None = None
|
||||
self._ircc_endpoint = SERVICE_IRCC
|
||||
self._commands: dict[str, str] = {}
|
||||
|
||||
async def connect(
|
||||
self,
|
||||
pin: str | None = None,
|
||||
psk: str | None = None,
|
||||
clientid: str | None = None,
|
||||
nickname: str | None = None,
|
||||
) -> None:
|
||||
"""Connect to device with PIN or PSK."""
|
||||
self._psk = pin[4:] if pin and pin[:4] == "psk:" else psk
|
||||
|
||||
_LOGGER.debug(
|
||||
"Connect with pin: %s, psk: %s, clientid: %s, nickname: %s",
|
||||
pin,
|
||||
self._psk,
|
||||
clientid,
|
||||
nickname,
|
||||
)
|
||||
|
||||
if self._psk is None:
|
||||
assert pin is not None
|
||||
assert clientid is not None
|
||||
assert nickname is not None
|
||||
await self.register(pin, clientid, nickname)
|
||||
else:
|
||||
if self._session is not None:
|
||||
self._auth = None
|
||||
self._session.cookie_jar.clear()
|
||||
|
||||
system_info = await self.get_system_info()
|
||||
if not system_info:
|
||||
raise BraviaNotSupported
|
||||
|
||||
if self._psk:
|
||||
_LOGGER.debug("Connected with PSK")
|
||||
else:
|
||||
_LOGGER.debug(
|
||||
"Connected with PIN, cookie len: %s", len(self._session._cookie_jar)
|
||||
)
|
||||
|
||||
async def register(self, pin: str, clientid: str, nickname: str) -> None:
|
||||
"""Register the device with PIN."""
|
||||
self._auth = BasicAuth("", pin)
|
||||
params = [
|
||||
{"clientid": clientid, "nickname": nickname, "level": "private"},
|
||||
[{"value": "yes", "function": "WOL"}],
|
||||
]
|
||||
await self.send_rest_req(
|
||||
SERVICE_ACCESS_CONTROL,
|
||||
"actRegister",
|
||||
params,
|
||||
)
|
||||
|
||||
async def pair(self, clientid: str, nickname: str) -> None:
|
||||
"""Register with PIN "0000" to start the pairing process on the TV."""
|
||||
with suppress(BraviaAuthError):
|
||||
await self.register(PAIR_PIN, clientid, nickname)
|
||||
|
||||
async def disconnect(self) -> None:
|
||||
"""Close connection."""
|
||||
if self._session:
|
||||
await self._session.close()
|
||||
self._auth = None
|
||||
self._psk = None
|
||||
self._session = None
|
||||
|
||||
async def send_wol_req(self) -> bool:
|
||||
"""Send WOL packet to device."""
|
||||
if self.mac is None:
|
||||
return False
|
||||
mac = self.mac.replace(":", "")
|
||||
packet = bytes.fromhex("F" * 12 + mac * 16)
|
||||
|
||||
with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock:
|
||||
sock.setsockopt(socket.SOL_SOCKET, socket.SO_BROADCAST, 1)
|
||||
sock.sendto(packet, ("<broadcast>", 9))
|
||||
return True
|
||||
|
||||
async def send_req(
|
||||
self,
|
||||
url: str,
|
||||
data: Any = None,
|
||||
headers: dict[str, Any] | None = None,
|
||||
json: bool = True,
|
||||
timeout: int = DEFAULT_TIMEOUT,
|
||||
) -> Any:
|
||||
"""Send HTTP request."""
|
||||
result = {} if json else False
|
||||
|
||||
if self._session is None:
|
||||
self._session = ClientSession(
|
||||
cookie_jar=CookieJar(unsafe=True, quote_cookie=False)
|
||||
)
|
||||
|
||||
if headers is None:
|
||||
headers = {}
|
||||
|
||||
if self._psk:
|
||||
headers["X-Auth-PSK"] = self._psk
|
||||
|
||||
headers["Cache-Control"] = "no-cache"
|
||||
headers["Connection"] = "keep-alive"
|
||||
|
||||
_LOGGER.debug("Request %s, data: %s, headers: %s", url, data, headers)
|
||||
|
||||
try:
|
||||
if json:
|
||||
# *hb* + ssl=False
|
||||
response = await self._session.post(
|
||||
url, json=data, headers=headers, timeout=timeout, auth=self._auth, ssl=False
|
||||
)
|
||||
else:
|
||||
# *hb* + ssl=False
|
||||
response = await self._session.post(
|
||||
url, data=data, headers=headers, timeout=timeout, auth=self._auth, ssl=False
|
||||
)
|
||||
|
||||
_LOGGER.debug("Response status: %s", response.status)
|
||||
|
||||
# Normalize non RFC-compliant cookie
|
||||
# https://github.com/Drafteed/pybravia/issues/1#issuecomment-1237452709
|
||||
cookies = response.headers.getall("set-cookie", None)
|
||||
if cookies:
|
||||
normalized_cookies = normalize_cookies(cookies)
|
||||
self._session.cookie_jar.update_cookies(
|
||||
cookies=normalized_cookies, response_url=response.url
|
||||
)
|
||||
|
||||
if response.status == 200:
|
||||
result = await response.json() if json else True
|
||||
_LOGGER.debug("Response result: %s", result)
|
||||
if response.status == 404:
|
||||
raise BraviaNotFound
|
||||
if response.status in [401, 403]:
|
||||
raise BraviaAuthError
|
||||
except ClientError as err:
|
||||
_LOGGER.debug("Request error %s", err)
|
||||
raise BraviaConnectionError from err
|
||||
except ConnectionError as err:
|
||||
_LOGGER.debug("Connection error %s", err)
|
||||
raise BraviaConnectionError from err
|
||||
except asyncio.exceptions.TimeoutError as err:
|
||||
_LOGGER.debug("Request timeout %s", err)
|
||||
raise BraviaConnectionTimeout from err
|
||||
|
||||
return result
|
||||
|
||||
async def send_ircc_req(self, code: str, timeout: int = DEFAULT_TIMEOUT) -> bool:
|
||||
"""Send IRCC request to device."""
|
||||
|
||||
# After about 13 minutes of inactivity, some TVs
|
||||
# ignores the first command without giving any error.
|
||||
# This make an empty request to 'wake up' the api.
|
||||
# https://github.com/dcnielsen90/python-bravia-tv/issues/15
|
||||
# The endpoint of some TVs is case sensitive, this also detects this.
|
||||
# https://github.com/home-assistant/core/issues/86132
|
||||
if code != "":
|
||||
time = datetime.now()
|
||||
if not self._ircc_time or (time - self._ircc_time) > timedelta(minutes=10):
|
||||
try:
|
||||
await self.send_ircc_req("")
|
||||
except BraviaNotFound:
|
||||
self._ircc_endpoint = self._ircc_endpoint.swapcase()
|
||||
except BraviaError:
|
||||
pass
|
||||
self._ircc_time = time
|
||||
|
||||
# *hb* http -> https
|
||||
url = f"https://{self.host}/sony/{self._ircc_endpoint}"
|
||||
headers = {
|
||||
"SOAPACTION": '"urn:schemas-sony-com:service:IRCC:1#X_SendIRCC"',
|
||||
"Content-Type": "text/xml; charset=UTF-8",
|
||||
}
|
||||
data = (
|
||||
"<s:Envelope"
|
||||
' xmlns:s="http://schemas.xmlsoap.org/soap/envelope/"'
|
||||
' s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">'
|
||||
"<s:Body>"
|
||||
'<u:X_SendIRCC xmlns:u="urn:schemas-sony-com:service:IRCC:1">'
|
||||
f"<IRCCCode>{code}</IRCCCode>"
|
||||
"</u:X_SendIRCC>"
|
||||
"</s:Body>"
|
||||
"</s:Envelope>"
|
||||
)
|
||||
|
||||
return await self.send_req(
|
||||
url=url, data=data, headers=headers, json=False, timeout=timeout
|
||||
)
|
||||
|
||||
async def send_rest_req(
|
||||
self,
|
||||
service: str,
|
||||
method: str,
|
||||
params: Any = None,
|
||||
headers: dict[str, Any] | None = None,
|
||||
version: str = DEFAULT_VERSION,
|
||||
timeout: int = DEFAULT_TIMEOUT,
|
||||
) -> Any:
|
||||
"""Send REST request to device."""
|
||||
# *hb* http -> https
|
||||
url = f"https://{self.host}/sony/{service}"
|
||||
params = params if isinstance(params, list) else [params] if params else []
|
||||
data = {
|
||||
"method": method,
|
||||
"params": params,
|
||||
"id": 1,
|
||||
"version": version,
|
||||
}
|
||||
|
||||
resp = await self.send_req(
|
||||
url=url, data=data, headers=headers, json=True, timeout=timeout
|
||||
)
|
||||
|
||||
if error := resp.get("error"):
|
||||
if "not power-on" in error:
|
||||
raise BraviaTurnedOff
|
||||
|
||||
return resp
|
||||
|
||||
async def send_rest_quick(self, *args: Any, **kwargs: Any) -> bool:
|
||||
"""Send and quick check REST request to device."""
|
||||
resp = await self.send_rest_req(*args, **kwargs)
|
||||
return "result" in resp
|
||||
|
||||
async def send_command(self, command: str) -> bool:
|
||||
"""Send IRCC command to device."""
|
||||
code = await self.get_command_code(command)
|
||||
if code is None:
|
||||
return False
|
||||
return await self.send_ircc_req(code)
|
||||
|
||||
async def get_sound_settings(self) -> str:
|
||||
"""Get current sound output."""
|
||||
resp = await self.send_rest_req(
|
||||
SERVICE_AUDIO,
|
||||
"getSoundSettings",
|
||||
{"target": "outputTerminal"},
|
||||
version="1.1",
|
||||
)
|
||||
result = resp.get("result", [[{}]])[0][0]
|
||||
return result.get("currentValue", "")
|
||||
|
||||
async def get_power_status(self) -> str:
|
||||
"""Get current power status."""
|
||||
resp = await self.send_rest_req(SERVICE_SYSTEM, "getPowerStatus", timeout=5)
|
||||
result = resp.get("result", [{}])[0]
|
||||
return result.get("status", "off")
|
||||
|
||||
async def get_power_saving_mode(self) -> str:
|
||||
"""Get current power saving mode."""
|
||||
resp = await self.send_rest_req(SERVICE_SYSTEM, "getPowerSavingMode")
|
||||
result = resp.get("result", [{}])[0]
|
||||
return result.get("mode", "")
|
||||
|
||||
async def get_system_info(self) -> dict[str, str]:
|
||||
"""Get general information of the device."""
|
||||
resp = await self.send_rest_req(SERVICE_SYSTEM, "getSystemInformation")
|
||||
result = resp.get("result", [{}])[0]
|
||||
self.mac = result.get("macAddr", self.mac)
|
||||
return result
|
||||
|
||||
async def get_api_info(self, services: list[str]) -> list[dict[str, Any]]:
|
||||
"""Get supported services and their information."""
|
||||
resp = await self.send_rest_req(
|
||||
SERVICE_GUIDE,
|
||||
"getSupportedApiInfo",
|
||||
{"services": services},
|
||||
)
|
||||
result = resp.get("result", [[]])[0]
|
||||
return result
|
||||
|
||||
async def get_wol_mode(self) -> bool:
|
||||
"""Get information on the device's WOL mode settings."""
|
||||
resp = await self.send_rest_req(SERVICE_SYSTEM, "getWolMode")
|
||||
result = resp.get("result", [{}])[0]
|
||||
return result.get("enabled", False)
|
||||
|
||||
async def get_led_status(self) -> dict[str, str]:
|
||||
"""Get the LED Indicator mode and status."""
|
||||
resp = await self.send_rest_req(SERVICE_SYSTEM, "getLEDIndicatorStatus")
|
||||
result = resp.get("result", [{}])[0]
|
||||
return result
|
||||
|
||||
async def get_remote_info(self) -> list[Any]:
|
||||
"""Get information of the device's remote controller."""
|
||||
resp = await self.send_rest_req(SERVICE_SYSTEM, "getRemoteControllerInfo")
|
||||
result = resp.get("result", [{}, []])
|
||||
return result
|
||||
|
||||
async def get_command_list(self) -> dict[str, str]:
|
||||
"""Get list of all IRCC commands."""
|
||||
result = await self.get_remote_info()
|
||||
return {x["name"]: x["value"] for x in result[1]}
|
||||
|
||||
async def get_command_code(self, command: str) -> str | None:
|
||||
"""Get IRCC command code by name."""
|
||||
if not self._commands:
|
||||
self._commands = await self.get_command_list()
|
||||
return self._commands.get(command)
|
||||
|
||||
async def get_volume_info(
|
||||
self, target: str = DEFAULT_AUDIO_TARGET
|
||||
) -> dict[str, Any]:
|
||||
"""Get the sound volume information with preferred target."""
|
||||
result = await self.get_volume_info_full()
|
||||
value = {}
|
||||
for output in result:
|
||||
value = output
|
||||
if target == output.get("target"):
|
||||
break
|
||||
return value
|
||||
|
||||
async def get_volume_info_full(self) -> list[dict[str, Any]]:
|
||||
"""Get information about the sound volume and mute status."""
|
||||
resp = await self.send_rest_req(SERVICE_AUDIO, "getVolumeInformation")
|
||||
result = resp.get("result", [[]])[0]
|
||||
return result
|
||||
|
||||
async def get_picture_setting(self, target: str = "") -> list[dict[str, Any]]:
|
||||
"""Get information about the picture settings."""
|
||||
resp = await self.send_rest_req(
|
||||
SERVICE_VIDEO, "getPictureQualitySettings", {"target": target}
|
||||
)
|
||||
result = resp.get("result", [[]])[0]
|
||||
return result
|
||||
|
||||
async def get_app_list(self) -> list[dict[str, str]]:
|
||||
"""Get list of applications."""
|
||||
resp = await self.send_rest_req(SERVICE_APP_CONTROL, "getApplicationList")
|
||||
result = resp.get("result", [[]])[0]
|
||||
return result
|
||||
|
||||
async def get_scheme_list(self) -> list[str]:
|
||||
"""Get list of schemes that the device can handle."""
|
||||
resp = await self.send_rest_req(SERVICE_AV_CONTENT, "getSchemeList")
|
||||
result = resp.get("result", [[]])[0]
|
||||
return [d["scheme"] for d in result if "scheme" in d]
|
||||
|
||||
async def get_source_list(self, scheme: str) -> list[str]:
|
||||
"""Get list of sources in the scheme."""
|
||||
resp = await self.send_rest_req(
|
||||
SERVICE_AV_CONTENT,
|
||||
"getSourceList",
|
||||
{"scheme": scheme},
|
||||
)
|
||||
result = resp.get("result", [[]])[0]
|
||||
return [d["source"] for d in result if "source" in d]
|
||||
|
||||
async def get_content_count(self, source: str) -> int:
|
||||
"""Get count of contents in the source."""
|
||||
|
||||
# Extended timeout due to
|
||||
# https://github.com/home-assistant/core/issues/91781#issuecomment-1532151925
|
||||
resp = await self.send_rest_req(
|
||||
SERVICE_AV_CONTENT,
|
||||
"getContentCount",
|
||||
{"source": source},
|
||||
timeout=20,
|
||||
)
|
||||
result = resp.get("result", [{}])[0]
|
||||
return result.get("count", 0)
|
||||
|
||||
async def get_content_list(
|
||||
self, source: str, index: int = 0, count: int = 50
|
||||
) -> list[dict[str, Any]]:
|
||||
"""Get list of contents in the source."""
|
||||
|
||||
# Extended timeout due to
|
||||
# https://github.com/home-assistant/core/issues/91781#issuecomment-1532168838
|
||||
resp = await self.send_rest_req(
|
||||
SERVICE_AV_CONTENT,
|
||||
"getContentList",
|
||||
{"source": source, "stIdx": index, "cnt": count},
|
||||
timeout=20,
|
||||
)
|
||||
result = resp.get("result", [[]])[0]
|
||||
return result
|
||||
|
||||
async def get_content_list_full(self, source: str) -> list[dict[str, Any]]:
|
||||
"""Get full list of contents in the source."""
|
||||
result = []
|
||||
total = await self.get_content_count(source)
|
||||
for index in range(0, total, 50):
|
||||
count = min(50, total - index)
|
||||
resp = await self.get_content_list(source, index, count)
|
||||
result.extend(resp)
|
||||
return result
|
||||
|
||||
async def get_content_list_all(self, scheme: str) -> list[dict[str, Any]]:
|
||||
"""Get full list of contents in the scheme."""
|
||||
result = []
|
||||
sources = await self.get_source_list(scheme)
|
||||
for source in sources:
|
||||
resp = await self.get_content_list_full(source)
|
||||
result.extend(resp)
|
||||
return result
|
||||
|
||||
async def get_external_status(
|
||||
self, version: str = DEFAULT_VERSION
|
||||
) -> list[dict[str, Any]]:
|
||||
"""Get information of all external input sources."""
|
||||
resp = await self.send_rest_req(
|
||||
SERVICE_AV_CONTENT,
|
||||
"getCurrentExternalInputsStatus",
|
||||
version=version,
|
||||
)
|
||||
result = resp.get("result", [[]])[0]
|
||||
return result
|
||||
|
||||
async def get_playing_info(self) -> dict[str, Any]:
|
||||
"""Get information of the currently playing content."""
|
||||
resp = await self.send_rest_req(SERVICE_AV_CONTENT, "getPlayingContentInfo")
|
||||
result = resp.get("result", [{}])[0]
|
||||
return result
|
||||
|
||||
async def set_picture_setting(self, target: str, value: str) -> bool:
|
||||
"""Set a picture setting to a new value."""
|
||||
return await self.send_rest_req(
|
||||
SERVICE_VIDEO,
|
||||
"setPictureQualitySettings",
|
||||
{"settings": [{"target": target, "value": value}]},
|
||||
version="1.0",
|
||||
)
|
||||
|
||||
async def set_wol_mode(self, mode: bool) -> bool:
|
||||
"""Set WOL mode settings of the device."""
|
||||
return await self.send_rest_quick(
|
||||
SERVICE_SYSTEM, "setWolMode", {"enabled": mode}
|
||||
)
|
||||
|
||||
async def set_led_status(self, mode: str, status: str | None = None) -> bool:
|
||||
"""Set the LED Indicator mode and status."""
|
||||
return await self.send_rest_quick(
|
||||
SERVICE_SYSTEM,
|
||||
"setLEDIndicatorStatus",
|
||||
{"mode": mode, "status": status},
|
||||
version="1.1",
|
||||
)
|
||||
|
||||
async def set_active_app(self, uri: str) -> bool:
|
||||
"""Launch an application by URI."""
|
||||
return await self.send_rest_quick(
|
||||
SERVICE_APP_CONTROL, "setActiveApp", {"uri": uri}
|
||||
)
|
||||
|
||||
async def set_play_content(self, uri: str) -> bool:
|
||||
"""Play content by URI."""
|
||||
return await self.send_rest_quick(
|
||||
SERVICE_AV_CONTENT, "setPlayContent", {"uri": uri}
|
||||
)
|
||||
|
||||
async def set_sound_settings(self, output: str) -> bool:
|
||||
"""Change current sound output."""
|
||||
return await self.send_rest_quick(
|
||||
SERVICE_AUDIO,
|
||||
"setSoundSettings",
|
||||
[{"settings": [{"value": output, "target": "outputTerminal"}]}],
|
||||
version="1.1",
|
||||
)
|
||||
|
||||
async def set_power_status(self, status: bool) -> bool:
|
||||
"""Change the current power status of the device."""
|
||||
return await self.send_rest_quick(
|
||||
SERVICE_SYSTEM, "setPowerStatus", {"status": status}
|
||||
)
|
||||
|
||||
async def set_power_saving_mode(self, mode: str) -> bool:
|
||||
"""Change the setting of the power saving mode."""
|
||||
return await self.send_rest_quick(
|
||||
SERVICE_SYSTEM, "setPowerSavingMode", {"mode": mode}
|
||||
)
|
||||
|
||||
async def set_text_form(self, text: str) -> bool:
|
||||
"""Input text on the field of the software keyboard."""
|
||||
return await self.send_rest_quick(SERVICE_APP_CONTROL, "setTextForm", text)
|
||||
|
||||
async def turn_on(self) -> bool:
|
||||
"""Turn on the device."""
|
||||
await self.send_wol_req()
|
||||
with suppress(BraviaError):
|
||||
if await self.get_power_status() != "active":
|
||||
with suppress(BraviaError):
|
||||
await self.set_power_status(True)
|
||||
with suppress(BraviaError):
|
||||
await self.send_ircc_req(CODE_POWER_ON)
|
||||
return True
|
||||
|
||||
async def turn_off(self) -> bool:
|
||||
"""Turn off the device."""
|
||||
return await self.set_power_status(False)
|
||||
|
||||
async def volume_up(self, step: int = 1, **kwargs: str) -> bool:
|
||||
"""Increase the volume."""
|
||||
level = f"+{step}"
|
||||
return await self.volume_level(level, **kwargs)
|
||||
|
||||
async def volume_down(self, step: int = 1, **kwargs: str) -> bool:
|
||||
"""Decrease the volume."""
|
||||
level = f"-{step}"
|
||||
return await self.volume_level(level, **kwargs)
|
||||
|
||||
async def volume_level(
|
||||
self,
|
||||
level: int | str,
|
||||
target: str = DEFAULT_AUDIO_TARGET,
|
||||
ui_mode: str | None = None,
|
||||
version: str = DEFAULT_VERSION,
|
||||
) -> bool:
|
||||
"""Change the audio volume level."""
|
||||
params = {"target": target, "volume": str(level)}
|
||||
if ui_mode is not None:
|
||||
params["ui"] = ui_mode
|
||||
return await self.send_rest_quick(
|
||||
SERVICE_AUDIO, "setAudioVolume", params, version=version
|
||||
)
|
||||
|
||||
async def volume_mute(self, mute: bool | None = None) -> bool:
|
||||
"""Change the audio mute status."""
|
||||
if mute is None:
|
||||
volume_info = await self.get_volume_info()
|
||||
mute = not volume_info.get("mute")
|
||||
return await self.send_rest_quick(
|
||||
SERVICE_AUDIO, "setAudioMute", {"status": mute}
|
||||
)
|
||||
|
||||
async def play(self) -> bool:
|
||||
"""Send play command."""
|
||||
return await self.send_command("Play")
|
||||
|
||||
async def pause(self) -> bool:
|
||||
"""Send pause command."""
|
||||
return await self.send_command("Pause")
|
||||
|
||||
async def stop(self) -> bool:
|
||||
"""Send stop command."""
|
||||
return await self.send_command("Stop")
|
||||
|
||||
async def next_track(self) -> bool:
|
||||
"""Send next track command."""
|
||||
return await self.send_command("Next")
|
||||
|
||||
async def previous_track(self) -> bool:
|
||||
"""Send previous track command."""
|
||||
return await self.send_command("Prev")
|
||||
|
||||
async def channel_up(self) -> bool:
|
||||
"""Send next channel command."""
|
||||
return await self.send_command("ChannelUp")
|
||||
|
||||
async def channel_down(self) -> bool:
|
||||
"""Send previous channel command."""
|
||||
return await self.send_command("ChannelDown")
|
||||
|
||||
async def terminate_apps(self) -> bool:
|
||||
"""Terminate all opened applications."""
|
||||
return await self.send_rest_quick(SERVICE_APP_CONTROL, "terminateApps")
|
||||
|
||||
async def reboot(self) -> bool:
|
||||
"""Send command to reboot the device."""
|
||||
return await self.send_rest_quick(SERVICE_SYSTEM, "requestReboot")
|
||||
|
||||
async def __aenter__(self) -> BraviaClient:
|
||||
"""Connect the client with context manager."""
|
||||
return self
|
||||
|
||||
async def __aexit__(
|
||||
self, exc_type: Exception, exc_value: str, traceback: TracebackType
|
||||
) -> None:
|
||||
"""Disconnect from context manager."""
|
||||
await self.disconnect()
|
||||
@@ -0,0 +1,21 @@
|
||||
"""Bravia constants."""
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Final
|
||||
|
||||
CODE_POWER_ON: Final = "AAAAAQAAAAEAAAAuAw=="
|
||||
|
||||
DEFAULT_AUDIO_TARGET: Final = "speaker"
|
||||
DEFAULT_TIMEOUT: Final = 10
|
||||
DEFAULT_VERSION: Final = "1.0"
|
||||
|
||||
PAIR_PIN: Final = "0000"
|
||||
|
||||
SERVICE_ACCESS_CONTROL: Final = "accessControl"
|
||||
SERVICE_APP_CONTROL: Final = "appControl"
|
||||
SERVICE_AUDIO: Final = "audio"
|
||||
SERVICE_VIDEO: Final = "video"
|
||||
SERVICE_AV_CONTENT: Final = "avContent"
|
||||
SERVICE_GUIDE: Final = "guide"
|
||||
SERVICE_IRCC: Final = "ircc"
|
||||
SERVICE_SYSTEM: Final = "system"
|
||||
@@ -0,0 +1,29 @@
|
||||
"""Bravia exceptions."""
|
||||
|
||||
|
||||
class BraviaError(Exception):
|
||||
"""Base Bravia exception."""
|
||||
|
||||
|
||||
class BraviaAuthError(BraviaError):
|
||||
"""Raised to indicate auth error."""
|
||||
|
||||
|
||||
class BraviaNotFound(BraviaError):
|
||||
"""Raised to indicate not found error."""
|
||||
|
||||
|
||||
class BraviaNotSupported(BraviaError):
|
||||
"""Raised to indicate not supported error."""
|
||||
|
||||
|
||||
class BraviaConnectionError(BraviaError):
|
||||
"""Raised to indicate connection error."""
|
||||
|
||||
|
||||
class BraviaConnectionTimeout(BraviaError):
|
||||
"""Raised to indicate connection timeout."""
|
||||
|
||||
|
||||
class BraviaTurnedOff(BraviaError):
|
||||
"""Raised to indicate that TV is turned off and do not respond."""
|
||||
Whitespace-only changes.
@@ -0,0 +1,17 @@
|
||||
"""Utils and helpers."""
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from http.cookies import SimpleCookie
|
||||
|
||||
REGEXP_COOKIE_EXPIRES = re.compile("(;\\s?expires=(.*)(;|$))", re.IGNORECASE)
|
||||
|
||||
|
||||
def normalize_cookies(cookies: list[str]) -> SimpleCookie:
|
||||
"""Normalize non RFC-compliant cookies."""
|
||||
result: SimpleCookie = SimpleCookie()
|
||||
|
||||
for cookie in cookies:
|
||||
result.load(REGEXP_COOKIE_EXPIRES.sub("", cookie))
|
||||
|
||||
return result
|
||||
Reference in new issue
Block a user