Skip to content

Commit

Permalink
Added sha256 support for newer models
Browse files Browse the repository at this point in the history
  • Loading branch information
SteveEasley committed Sep 22, 2024
1 parent 9522254 commit 05d8e86
Show file tree
Hide file tree
Showing 7 changed files with 50 additions and 13 deletions.
9 changes: 9 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ https://pypi.org/project/pyjvcprojector/

## Features

A full reference to the available commands is available from JVC here
http://pro.jvc.com/pro/attributes/PRESENT/Manual/External%20Command%20Spec%20for%20D-ILA%20projector_V3.0.pdf.

### Convenience functions:
* `JvcProjector::power_on()` turns on power.
* `JvcProjector::power_off()` turns off power.
Expand Down Expand Up @@ -77,3 +80,9 @@ async def main():

await jp.disconnect()
```

Password authentication is also supported for both older and newer models.

```python
JvcProjector("127.0.0.1", password="1234567890")
```
2 changes: 1 addition & 1 deletion jvcprojector/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,4 @@
)
from .projector import JvcProjector

__version__ = "1.0.12"
__version__ = "1.1.0"
2 changes: 2 additions & 0 deletions jvcprojector/command.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,8 @@
INPUT: Final = "IP"
REMOTE: Final = "RC"

AUTH_SALT: Final = "JVCKWPJ"


class JvcCommand:
"""Class for representing a JVC Projector command."""
Expand Down
28 changes: 19 additions & 9 deletions jvcprojector/device.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,12 @@
import asyncio
import logging
import struct
from hashlib import sha256
from time import time

from . import const
from .command import (
AUTH_SALT,
END,
HEAD_ACK,
HEAD_LEN,
Expand Down Expand Up @@ -45,7 +47,7 @@ def __init__(

self._auth = b""
if password:
self._auth = b"_" + struct.pack("10s", password.encode())
self._auth = struct.pack("10s", password.encode())

self._lock = asyncio.Lock()
self._keepalive: asyncio.Task | None = None
Expand Down Expand Up @@ -138,20 +140,28 @@ async def _connect(self) -> None:
raise JvcProjectorConnectError("Retries exceeded")

_LOGGER.debug("Handshake sending '%s'", PJREQ.decode())
await self._conn.write(PJREQ + self._auth)
await self._conn.write(PJREQ + (b"_" + self._auth if self._auth else b""))

try:
data = await self._conn.read(len(PJACK))
except asyncio.TimeoutError as err:
raise JvcProjectorConnectError("Handshake ack timeout") from err
_LOGGER.debug("Handshake received %s", data)

_LOGGER.debug("Handshake received %s", data)
if data == PJNAK:
_LOGGER.debug("Standard auth failed, trying SHA256 auth")
auth = sha256(f"{self._auth.decode()}{AUTH_SALT}".encode()).hexdigest().encode()
await self._conn.write(PJREQ + b"_" + auth)
data = await self._conn.read(len(PJACK))
if data == PJACK:
self._auth = auth

if data == PJNAK:
raise JvcProjectorAuthError()
if data == PJNAK:
raise JvcProjectorAuthError()

if data != PJACK:
raise JvcProjectorCommandError("Handshake ack invalid")
if data != PJACK:
raise JvcProjectorCommandError("Handshake ack invalid")

except asyncio.TimeoutError as err:
raise JvcProjectorConnectError("Handshake ack timeout") from err

self._last = time()

Expand Down
2 changes: 1 addition & 1 deletion jvcprojector/projector.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ async def get_info(self) -> dict[str, str]:

return {"model": self._model, "mac": self._mac}

async def get_state(self) -> dict[str, str]:
async def get_state(self) -> dict[str, str | None]:
"""Get device state."""
assert self._device
pwr = JvcCommand(command.POWER, True)
Expand Down
17 changes: 17 additions & 0 deletions tests/test_device.py
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
"""Tests for device module."""

from hashlib import sha256
from unittest.mock import AsyncMock, call

import pytest

from jvcprojector import command, const
from jvcprojector.command import JvcCommand
from jvcprojector.device import (
AUTH_SALT,
HEAD_ACK,
HEAD_OP,
HEAD_REF,
HEAD_RES,
PJACK,
PJNAK,
PJNG,
PJOK,
PJREQ,
Expand Down Expand Up @@ -76,6 +79,20 @@ async def test_send_with_password10(conn: AsyncMock):
)


@pytest.mark.asyncio
async def test_send_with_password_sha256(conn: AsyncMock):
"""Test send with a projector requiring sha256 hashing."""
conn.read.side_effect = [PJOK, PJNAK, PJACK]
dev = JvcDevice(IP, PORT, TIMEOUT, "passwd7890")
cmd = JvcCommand(f"{command.POWER}1")
await dev.send([cmd])
await dev.disconnect()
auth = sha256(f"passwd7890{AUTH_SALT}".encode()).hexdigest().encode()
conn.write.assert_has_calls(
[call(PJREQ + b"_" + auth), call(cc(HEAD_OP, f"{command.POWER}1"))]
)


@pytest.mark.asyncio
@pytest.mark.parametrize("conn", [{"raise_on_connect": 1}], indirect=True)
async def test_connection_refused_retry(conn: AsyncMock):
Expand Down
3 changes: 1 addition & 2 deletions tests/test_projector.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
"""Tests for projector module."""

import hashlib
from unittest.mock import AsyncMock

import pytest
Expand All @@ -9,7 +8,7 @@
from jvcprojector.error import JvcProjectorError
from jvcprojector.projector import JvcProjector

from . import IP, HOST, MAC, MODEL, PORT
from . import HOST, IP, MAC, MODEL, PORT


@pytest.mark.asyncio
Expand Down

0 comments on commit 05d8e86

Please sign in to comment.