From: 
Subject: Debian changes

The Debian packaging of hass-nabucasa is maintained in git, using a workflow
similar to the one described in dgit-maint-merge(7).
The Debian delta is represented by this one combined patch; there isn't a
patch queue that can be represented as a quilt series.

A detailed breakdown of the changes is available from their canonical
representation -- git commits in the packaging repository.
For example, to see the changes made by the Debian maintainer in the first
upload of upstream version 1.2.3, you could use:

    % git clone https://git.dgit.debian.org/hass-nabucasa
    % cd hass-nabucasa
    % git log --oneline 1.2.3..debian/1.2.3-1 -- . ':!debian'

(If you have dgit, use `dgit clone hass-nabucasa`, rather than plain `git clone`.)

We don't use debian/source/options single-debian-patch because it has bugs.
Therefore, NMUs etc. may nevertheless have made additional patches.

---

diff --git a/hass_nabucasa/instance_api.py b/hass_nabucasa/instance_api.py
index 7927086..d341417 100644
--- a/hass_nabucasa/instance_api.py
+++ b/hass_nabucasa/instance_api.py
@@ -57,6 +57,7 @@ class InstanceSnitunTokenDetails(TypedDict):
     server: str
     valid: int
     throttling: int
+    protocol_version: NotRequired[int]
 
 
 class PingResult(TypedDict):
diff --git a/hass_nabucasa/remote.py b/hass_nabucasa/remote.py
index 8d23e50..aae9862 100644
--- a/hass_nabucasa/remote.py
+++ b/hass_nabucasa/remote.py
@@ -13,6 +13,7 @@ from typing import TYPE_CHECKING, TypedDict
 import aiohttp
 import attr
 from snitun.exceptions import SniTunConnectionError
+from snitun.utils import DEFAULT_PROTOCOL_VERSION
 from snitun.utils.aes import generate_aes_keyset
 from snitun.utils.aiohttp_client import SniTunClientAioHttp
 
@@ -77,6 +78,7 @@ class SniTunToken:
     aes_iv = attr.ib(type=bytes)
     valid = attr.ib(type=datetime)
     throttling = attr.ib(type=int)
+    protocol_version = attr.ib(type=int, default=DEFAULT_PROTOCOL_VERSION)
 
 
 @attr.s
@@ -402,7 +404,7 @@ class RemoteUI:
 
         _LOGGER.debug("Starting SniTun")
         is_cloud_request.set(True)
-        await self._snitun.start(False, self._recreate_backend)
+        await self._snitun.start()
         self.cloud.client.dispatcher_message(const.DISPATCH_REMOTE_BACKEND_UP)
 
         _LOGGER.debug(
@@ -477,6 +479,7 @@ class RemoteUI:
             aes_iv,
             utils.utc_from_timestamp(data["valid"]),
             data["throttling"],
+            data.get("protocol_version", DEFAULT_PROTOCOL_VERSION),
         )
 
     async def connect(self) -> None:
@@ -505,6 +508,7 @@ class RemoteUI:
                     self._token.aes_key,
                     self._token.aes_iv,
                     throttling=self._token.throttling,
+                    protocol_version=self._token.protocol_version,
                 )
             _LOGGER.debug("Connected")
 
diff --git a/hass_nabucasa/utils.py b/hass_nabucasa/utils.py
index 0489b97..bb1319e 100644
--- a/hass_nabucasa/utils.py
+++ b/hass_nabucasa/utils.py
@@ -19,6 +19,7 @@ from .exceptions import NabuCasaBaseError
 
 CALLABLE_T = TypeVar("CALLABLE_T", bound=Callable)  # pylint: disable=invalid-name
 UTC = dt.UTC
+UNIX_EPOCH = dt.datetime(1970, 1, 1, tzinfo=UTC)
 
 _LOGGER = logging.getLogger(__name__)
 
@@ -48,7 +49,7 @@ def utcnow() -> dt.datetime:
 
 def utc_from_timestamp(timestamp: float) -> dt.datetime:
     """Return a UTC time from a timestamp."""
-    return dt.datetime.fromtimestamp(timestamp, UTC)
+    return UNIX_EPOCH + dt.timedelta(seconds=timestamp)
 
 
 def parse_date(dt_str: str) -> dt.date | None:
diff --git a/pyproject.toml b/pyproject.toml
index 6e8ebbb..47b8d48 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -30,7 +30,7 @@ dependencies = [
     "pycognito==2024.5.1",
     "PyJWT>=2.8.0",
     "requests>=2.0,<3",
-    "snitun==0.45.1",
+    "snitun==0.46.1",
     "voluptuous>=0.15",
     "webrtc-models<1.0.0",
     "yarl>=1.20,<2",
diff --git a/tests/common.py b/tests/common.py
index d9a704a..3f660b3 100644
--- a/tests/common.py
+++ b/tests/common.py
@@ -3,7 +3,6 @@
 from __future__ import annotations
 
 import asyncio
-from collections.abc import Coroutine
 import json
 from pathlib import Path
 import threading
@@ -16,6 +15,7 @@ from freezegun.api import (
     TickingDateTimeFactory,
 )
 import pytest
+from snitun.client.access_list import AccessList
 
 from hass_nabucasa.client import CloudClient
 
@@ -263,11 +263,11 @@ class MockSnitun:
         self.call_disconnect = False
         self.init_args = None
         self.connect_args = None
+        self.connect_kwargs = None
         self.init_kwarg = None
         self.wait_task = asyncio.Event()
 
-        self.start_whitelist = None
-        self.start_endpoint_connection_error_callback = None
+        self.start_access_list = None
 
     @property
     def is_connected(self):
@@ -280,14 +280,10 @@ class MockSnitun:
 
     async def start(
         self,
-        whitelist: bool = False,
-        endpoint_connection_error_callback: Coroutine[Any, Any, None] | None = None,
-    ):
+        access_list: AccessList | None = None,
+    ) -> None:
         """Start snitun."""
-        self.start_whitelist = whitelist
-        self.start_endpoint_connection_error_callback = (
-            endpoint_connection_error_callback
-        )
+        self.start_access_list = access_list
         self.call_start = True
 
     async def stop(self):
@@ -299,11 +295,16 @@ class MockSnitun:
         token: bytes,
         aes_key: bytes,
         aes_iv: bytes,
-        throttling=None,
-    ):
+        throttling: int | None = None,
+        protocol_version: int = 0,
+    ) -> None:
         """Connect snitun."""
         self.call_connect = True
         self.connect_args = [token, aes_key, aes_iv, throttling]
+        self.connect_kwargs = {
+            "throttling": throttling,
+            "protocol_version": protocol_version,
+        }
 
     async def disconnect(self):
         """Disconnect snitun."""
diff --git a/tests/conftest.py b/tests/conftest.py
index fa519be..84a03e4 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -55,7 +55,10 @@ async def loop_fixture():
 async def aioclient_mock(loop):
     """Fixture to mock aioclient calls."""
     with mock_aiohttp_client(loop) as mock_session:
-        yield mock_session
+        try:
+            yield mock_session
+        finally:
+            await mock_session.close_sessions()
 
 
 @pytest.fixture
diff --git a/tests/test_remote.py b/tests/test_remote.py
index 3d7c3fe..0c58f48 100644
--- a/tests/test_remote.py
+++ b/tests/test_remote.py
@@ -12,6 +12,7 @@ from acme import client, messages
 import aiohttp
 import pytest
 from requests.exceptions import RequestException
+from snitun.utils import DEFAULT_PROTOCOL_VERSION
 from syrupy import SnapshotAssertion
 
 from hass_nabucasa import Cloud, utils
@@ -151,13 +152,14 @@ async def test_load_backend_exists_cert(
         "snitun_port": 443,
     }
 
-    assert snitun_mock.start_whitelist is not None
-    assert snitun_mock.start_endpoint_connection_error_callback is not None
+    assert snitun_mock.start_access_list is None
 
     await asyncio.sleep(0.1)
     assert snitun_mock.call_connect
     assert snitun_mock.connect_args[0] == b"test-token"
     assert snitun_mock.connect_args[3] == 400
+    # No protocol_version in the token response -> library default
+    assert snitun_mock.connect_kwargs["protocol_version"] == DEFAULT_PROTOCOL_VERSION
     assert cloud.remote.is_connected
 
     assert cloud.remote._acme_task
@@ -188,6 +190,45 @@ async def test_load_backend_exists_cert(
     assert cloud.remote.certificate_status == CertificateStatus.READY
 
 
+async def test_load_backend_with_protocol_version(
+    cloud: Cloud,
+    valid_acme_mock: MockAcme,
+    aioclient_mock: AiohttpClientMocker,
+    snitun_mock: MockSnitun,
+) -> None:
+    """Connect uses the protocol_version provided by the token response."""
+    valid = utcnow() + timedelta(days=1)
+
+    aioclient_mock.post(
+        f"https://{cloud.api_server}/instance/register",
+        json={
+            "domain": "test.dui.nabu.casa",
+            "email": "test@nabucasa.inc",
+            "server": "rest-remote.nabu.casa",
+        },
+    )
+    aioclient_mock.post(
+        f"https://{cloud.api_server}/instance/snitun_token",
+        json={
+            "token": "test-token",
+            "server": "rest-remote.nabu.casa",
+            "valid": valid.timestamp(),
+            "throttling": 400,
+            "protocol_version": 2,
+        },
+    )
+
+    await cloud.remote.start()
+    await cloud.remote._info_loaded.wait()
+    await asyncio.sleep(0.1)
+
+    assert snitun_mock.call_connect
+    assert snitun_mock.connect_kwargs["protocol_version"] == 2
+
+    await cloud.remote.stop()
+    await asyncio.sleep(0.1)
+
+
 async def test_load_backend_not_exists_cert(
     cloud: Cloud,
     acme_mock: MockAcme,
diff --git a/tests/test_utils.py b/tests/test_utils.py
index d918ad2..f5de183 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -10,6 +10,14 @@ from syrupy import SnapshotAssertion
 from hass_nabucasa import utils
 
 
+def test_utc_from_timestamp_after_32bit_time_t() -> None:
+    """Test utc_from_timestamp handles timestamps after 2038 on 32-bit platforms."""
+    assert (
+        utils.utc_from_timestamp(9999999999).isoformat()
+        == "2286-11-20T17:46:39+00:00"
+    )
+
+
 @pytest.mark.parametrize(
     "input_str",
     [
diff --git a/tests/utils/aiohttp.py b/tests/utils/aiohttp.py
index 0f33e52..af87938 100644
--- a/tests/utils/aiohttp.py
+++ b/tests/utils/aiohttp.py
@@ -34,6 +34,7 @@ class AiohttpClientMocker:
         self._mocks = []
         self._cookies = {}
         self.mock_calls = []
+        self._sessions = []
 
     def request(
         self,
@@ -110,10 +111,17 @@ class AiohttpClientMocker:
     def create_session(self, loop):
         """Create a ClientSession that is bound to this mocker."""
         session = ClientSession(loop=loop)
+        self._sessions.append(session)
         # Setting directly on `session` will raise deprecation warning
         object.__setattr__(session, "_request", self.match_request)
         return session
 
+    async def close_sessions(self):
+        """Close all sessions created by this mocker."""
+        for session in self._sessions:
+            if not session.closed:
+                await session.close()
+
     async def match_request(
         self,
         method,
