private-assistant-display-controller 0.2.0__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.0 → private_assistant_display_controller-0.2.2}/PKG-INFO +1 -1
  2. {private_assistant_display_controller-0.2.0 → private_assistant_display_controller-0.2.2}/pyproject.toml +1 -1
  3. {private_assistant_display_controller-0.2.0 → private_assistant_display_controller-0.2.2}/src/private_assistant_display_controller/__init__.py +1 -1
  4. {private_assistant_display_controller-0.2.0 → private_assistant_display_controller-0.2.2}/src/private_assistant_display_controller/controller.py +0 -1
  5. {private_assistant_display_controller-0.2.0 → private_assistant_display_controller-0.2.2}/src/private_assistant_display_controller/display.py +22 -19
  6. {private_assistant_display_controller-0.2.0 → private_assistant_display_controller-0.2.2}/LICENSE +0 -0
  7. {private_assistant_display_controller-0.2.0 → private_assistant_display_controller-0.2.2}/README.md +0 -0
  8. {private_assistant_display_controller-0.2.0 → private_assistant_display_controller-0.2.2}/src/private_assistant_display_controller/config.py +0 -0
  9. {private_assistant_display_controller-0.2.0 → private_assistant_display_controller-0.2.2}/src/private_assistant_display_controller/exceptions.py +0 -0
  10. {private_assistant_display_controller-0.2.0 → private_assistant_display_controller-0.2.2}/src/private_assistant_display_controller/main.py +0 -0
  11. {private_assistant_display_controller-0.2.0 → private_assistant_display_controller-0.2.2}/src/private_assistant_display_controller/models.py +0 -0
  12. {private_assistant_display_controller-0.2.0 → private_assistant_display_controller-0.2.2}/src/private_assistant_display_controller/mqtt_client.py +0 -0
  13. {private_assistant_display_controller-0.2.0 → private_assistant_display_controller-0.2.2}/src/private_assistant_display_controller/py.typed +0 -0
  14. {private_assistant_display_controller-0.2.0 → 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.0
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.0"
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.0"
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,12 +84,12 @@ class InkyDisplay(DisplayInterface):
85
84
 
86
85
  @property
87
86
  def width(self) -> int:
88
- """Display width in pixels."""
87
+ """Display width in pixels (hardware landscape dimension)."""
89
88
  return self._width
90
89
 
91
90
  @property
92
91
  def height(self) -> int:
93
- """Display height in pixels."""
92
+ """Display height in pixels (hardware landscape dimension)."""
94
93
  return self._height
95
94
 
96
95
  async def show_image(self, image: Image.Image, saturation: float = 0.5) -> None:
@@ -134,16 +133,21 @@ 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
 
147
+ display_image = image
148
+
145
149
  logger.info("Updating display (this takes ~20-25 seconds)...")
146
- self._display.set_image(image, saturation=saturation) # type: ignore[union-attr]
150
+ self._display.set_image(display_image, saturation=saturation) # type: ignore[union-attr]
147
151
  try:
148
152
  self._display.show(busy_wait=True) # type: ignore[union-attr]
149
153
  except FileNotFoundError as e:
@@ -215,12 +219,13 @@ class MockDisplay(DisplayInterface):
215
219
  """
216
220
  _ = saturation # Unused in mock, but part of interface
217
221
 
218
- # 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
+
219
226
  if image.size != (self._width, self._height):
220
227
  raise DisplayError(
221
- f"Image size {image.size[0]}x{image.size[1]} does not match "
222
- f"display size {self._width}x{self._height}. "
223
- "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}."
224
229
  )
225
230
 
226
231
  self._last_image = image.copy()
@@ -239,7 +244,6 @@ class MockDisplay(DisplayInterface):
239
244
 
240
245
  def create_display(
241
246
  mock: bool = False,
242
- orientation: Literal["landscape", "portrait"] = "landscape",
243
247
  mock_width: int = 1600,
244
248
  mock_height: int = 1200,
245
249
  ) -> DisplayInterface:
@@ -247,7 +251,6 @@ def create_display(
247
251
 
248
252
  Args:
249
253
  mock: If True, create a MockDisplay for testing.
250
- orientation: Display orientation for real hardware.
251
254
  mock_width: Width for mock display (ignored for real hardware).
252
255
  mock_height: Height for mock display (ignored for real hardware).
253
256
 
@@ -262,5 +265,5 @@ def create_display(
262
265
  logger.info("Creating mock display (%dx%d)", mock_width, mock_height)
263
266
  return MockDisplay(width=mock_width, height=mock_height)
264
267
 
265
- logger.info("Creating Inky display with orientation: %s", orientation)
266
- return InkyDisplay(orientation=orientation)
268
+ logger.info("Creating Inky display")
269
+ return InkyDisplay()