private-assistant-display-controller 0.2.1__tar.gz → 0.2.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (14) hide show
  1. {private_assistant_display_controller-0.2.1 → private_assistant_display_controller-0.2.2}/PKG-INFO +1 -1
  2. {private_assistant_display_controller-0.2.1 → private_assistant_display_controller-0.2.2}/pyproject.toml +1 -1
  3. {private_assistant_display_controller-0.2.1 → private_assistant_display_controller-0.2.2}/src/private_assistant_display_controller/__init__.py +1 -1
  4. {private_assistant_display_controller-0.2.1 → private_assistant_display_controller-0.2.2}/src/private_assistant_display_controller/controller.py +0 -1
  5. {private_assistant_display_controller-0.2.1 → private_assistant_display_controller-0.2.2}/src/private_assistant_display_controller/display.py +24 -29
  6. {private_assistant_display_controller-0.2.1 → private_assistant_display_controller-0.2.2}/LICENSE +0 -0
  7. {private_assistant_display_controller-0.2.1 → private_assistant_display_controller-0.2.2}/README.md +0 -0
  8. {private_assistant_display_controller-0.2.1 → private_assistant_display_controller-0.2.2}/src/private_assistant_display_controller/config.py +0 -0
  9. {private_assistant_display_controller-0.2.1 → private_assistant_display_controller-0.2.2}/src/private_assistant_display_controller/exceptions.py +0 -0
  10. {private_assistant_display_controller-0.2.1 → private_assistant_display_controller-0.2.2}/src/private_assistant_display_controller/main.py +0 -0
  11. {private_assistant_display_controller-0.2.1 → private_assistant_display_controller-0.2.2}/src/private_assistant_display_controller/models.py +0 -0
  12. {private_assistant_display_controller-0.2.1 → private_assistant_display_controller-0.2.2}/src/private_assistant_display_controller/mqtt_client.py +0 -0
  13. {private_assistant_display_controller-0.2.1 → private_assistant_display_controller-0.2.2}/src/private_assistant_display_controller/py.typed +0 -0
  14. {private_assistant_display_controller-0.2.1 → private_assistant_display_controller-0.2.2}/src/private_assistant_display_controller/s3_client.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: private-assistant-display-controller
3
- Version: 0.2.1
3
+ Version: 0.2.2
4
4
  Summary: Package is intended to be used in conjunction with the display skill to control a display device.
5
5
  Keywords:
6
6
  Author: sktr22
@@ -4,7 +4,7 @@ build-backend = "uv_build"
4
4
 
5
5
  [project]
6
6
  name = "private-assistant-display-controller"
7
- version = "0.2.1"
7
+ version = "0.2.2"
8
8
  description = "Package is intended to be used in conjunction with the display skill to control a display device."
9
9
  authors = [
10
10
  { name = "sktr22", email = "sktr22@github.com" }
@@ -4,7 +4,7 @@ This package provides a daemon that receives MQTT commands from the
4
4
  picture-display-skill and displays images on an Inky Impression e-ink display.
5
5
  """
6
6
 
7
- __version__ = "0.2.1"
7
+ __version__ = "0.2.2"
8
8
 
9
9
  from private_assistant_display_controller.config import Settings, load_settings
10
10
  from private_assistant_display_controller.controller import DisplayController
@@ -41,7 +41,6 @@ class DisplayController:
41
41
  self._s3 = S3ImageClient()
42
42
  self._display: DisplayInterface = create_display(
43
43
  mock=settings.display.mock,
44
- orientation=settings.display.orientation,
45
44
  mock_width=settings.display.mock_width,
46
45
  mock_height=settings.display.mock_height,
47
46
  )
@@ -4,7 +4,6 @@ import asyncio
4
4
  import logging
5
5
  from abc import ABC, abstractmethod
6
6
  from concurrent.futures import ThreadPoolExecutor
7
- from typing import Literal
8
7
 
9
8
  from PIL import Image
10
9
 
@@ -48,11 +47,13 @@ class InkyDisplay(DisplayInterface):
48
47
  so it runs in a dedicated thread pool to avoid blocking the async loop.
49
48
 
50
49
  Dimensions are auto-detected from the hardware during initialization.
50
+ The Inky library itself handles any internal rotation required by the
51
+ physical mounting — the public API always expects landscape images
52
+ (wider than tall).
51
53
  """
52
54
 
53
55
  def __init__(
54
56
  self,
55
- orientation: Literal["landscape", "portrait"] = "landscape",
56
57
  executor: ThreadPoolExecutor | None = None,
57
58
  ) -> None:
58
59
  """Initialize the Inky display wrapper.
@@ -60,14 +61,12 @@ class InkyDisplay(DisplayInterface):
60
61
  Connects to hardware immediately to detect display dimensions.
61
62
 
62
63
  Args:
63
- orientation: Display orientation.
64
64
  executor: Optional thread pool executor for display operations.
65
65
 
66
66
  Raises:
67
67
  DisplayError: If the display cannot be initialized.
68
68
 
69
69
  """
70
- self._orientation = orientation
71
70
  self._executor = executor or ThreadPoolExecutor(max_workers=1, thread_name_prefix="inky")
72
71
  self._lock = asyncio.Lock()
73
72
 
@@ -85,13 +84,13 @@ class InkyDisplay(DisplayInterface):
85
84
 
86
85
  @property
87
86
  def width(self) -> int:
88
- """Logical display width in pixels (accounts for orientation)."""
89
- return self._height if self._orientation == "portrait" else self._width
87
+ """Display width in pixels (hardware landscape dimension)."""
88
+ return self._width
90
89
 
91
90
  @property
92
91
  def height(self) -> int:
93
- """Logical display height in pixels (accounts for orientation)."""
94
- return self._width if self._orientation == "portrait" else self._height
92
+ """Display height in pixels (hardware landscape dimension)."""
93
+ return self._height
95
94
 
96
95
  async def show_image(self, image: Image.Image, saturation: float = 0.5) -> None:
97
96
  """Display an image on the Inky screen.
@@ -134,19 +133,18 @@ class InkyDisplay(DisplayInterface):
134
133
  DisplayError: If image dimensions don't match display dimensions.
135
134
 
136
135
  """
137
- # Validate image dimensions - skill is responsible for correct sizing
136
+ # Normalise to landscape: the Inky library always expects the wider
137
+ # dimension as width, and handles any physical mounting rotation
138
+ # internally (hard-coded rot90 in InkyEL133UF1.show()).
139
+ if image.height > image.width:
140
+ image = image.transpose(Image.Transpose.ROTATE_90)
141
+
138
142
  if image.size != (self.width, self.height):
139
143
  raise DisplayError(
140
- f"Image size {image.size[0]}x{image.size[1]} does not match "
141
- f"display size {self.width}x{self.height}. "
142
- "The skill must provide correctly sized images."
144
+ f"Image size {image.size[0]}x{image.size[1]} does not match display size {self.width}x{self.height}."
143
145
  )
144
146
 
145
- # For portrait, rotate to physical hardware dimensions before sending.
146
- # Hardware always expects landscape (raw) dimensions; ROTATE_90 is 90°
147
- # counter-clockwise — adjust if the physical mounting requires the
148
- # opposite direction.
149
- display_image = image.transpose(Image.Transpose.ROTATE_90) if self._orientation == "portrait" else image
147
+ display_image = image
150
148
 
151
149
  logger.info("Updating display (this takes ~20-25 seconds)...")
152
150
  self._display.set_image(display_image, saturation=saturation) # type: ignore[union-attr]
@@ -221,12 +219,13 @@ class MockDisplay(DisplayInterface):
221
219
  """
222
220
  _ = saturation # Unused in mock, but part of interface
223
221
 
224
- # Validate image dimensions - same behavior as real display
222
+ # Normalise to landscape, same as the real display
223
+ if image.height > image.width:
224
+ image = image.transpose(Image.Transpose.ROTATE_90)
225
+
225
226
  if image.size != (self._width, self._height):
226
227
  raise DisplayError(
227
- f"Image size {image.size[0]}x{image.size[1]} does not match "
228
- f"display size {self._width}x{self._height}. "
229
- "The skill must provide correctly sized images."
228
+ f"Image size {image.size[0]}x{image.size[1]} does not match display size {self._width}x{self._height}."
230
229
  )
231
230
 
232
231
  self._last_image = image.copy()
@@ -245,7 +244,6 @@ class MockDisplay(DisplayInterface):
245
244
 
246
245
  def create_display(
247
246
  mock: bool = False,
248
- orientation: Literal["landscape", "portrait"] = "landscape",
249
247
  mock_width: int = 1600,
250
248
  mock_height: int = 1200,
251
249
  ) -> DisplayInterface:
@@ -253,7 +251,6 @@ def create_display(
253
251
 
254
252
  Args:
255
253
  mock: If True, create a MockDisplay for testing.
256
- orientation: Display orientation for real hardware.
257
254
  mock_width: Width for mock display (ignored for real hardware).
258
255
  mock_height: Height for mock display (ignored for real hardware).
259
256
 
@@ -265,10 +262,8 @@ def create_display(
265
262
 
266
263
  """
267
264
  if mock:
268
- # Swap to logical (portrait) dimensions so the mock mirrors InkyDisplay behaviour.
269
- w, h = (mock_height, mock_width) if orientation == "portrait" else (mock_width, mock_height)
270
- logger.info("Creating mock display (%dx%d)", w, h)
271
- return MockDisplay(width=w, height=h)
265
+ logger.info("Creating mock display (%dx%d)", mock_width, mock_height)
266
+ return MockDisplay(width=mock_width, height=mock_height)
272
267
 
273
- logger.info("Creating Inky display with orientation: %s", orientation)
274
- return InkyDisplay(orientation=orientation)
268
+ logger.info("Creating Inky display")
269
+ return InkyDisplay()