python-openhab-rest-client 21.4.0__tar.gz → 21.4.1__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 (139) hide show
  1. {python_openhab_rest_client-21.4.0/python_openhab_rest_client.egg-info → python_openhab_rest_client-21.4.1}/PKG-INFO +92 -1
  2. {python_openhab_rest_client-21.4.0 → python_openhab_rest_client-21.4.1}/README.md +91 -0
  3. {python_openhab_rest_client-21.4.0 → python_openhab_rest_client-21.4.1}/openhab/AsyncLinks.py +2 -1
  4. {python_openhab_rest_client-21.4.0 → python_openhab_rest_client-21.4.1}/openhab/AsyncPersistence.py +2 -1
  5. {python_openhab_rest_client-21.4.0 → python_openhab_rest_client-21.4.1}/openhab/AsyncRules.py +1 -1
  6. {python_openhab_rest_client-21.4.0 → python_openhab_rest_client-21.4.1}/openhab/AsyncSitemaps.py +29 -66
  7. {python_openhab_rest_client-21.4.0 → python_openhab_rest_client-21.4.1}/openhab/AsyncVoice.py +4 -2
  8. {python_openhab_rest_client-21.4.0 → python_openhab_rest_client-21.4.1}/openhab/Links.py +2 -1
  9. {python_openhab_rest_client-21.4.0 → python_openhab_rest_client-21.4.1}/openhab/Persistence.py +2 -1
  10. {python_openhab_rest_client-21.4.0 → python_openhab_rest_client-21.4.1}/openhab/Rules.py +1 -1
  11. {python_openhab_rest_client-21.4.0 → python_openhab_rest_client-21.4.1}/openhab/Sitemaps.py +29 -66
  12. {python_openhab_rest_client-21.4.0 → python_openhab_rest_client-21.4.1}/openhab/Voice.py +4 -2
  13. {python_openhab_rest_client-21.4.0 → python_openhab_rest_client-21.4.1}/openhab/__init__.py +1 -1
  14. {python_openhab_rest_client-21.4.0 → python_openhab_rest_client-21.4.1/python_openhab_rest_client.egg-info}/PKG-INFO +92 -1
  15. {python_openhab_rest_client-21.4.0 → python_openhab_rest_client-21.4.1}/python_openhab_rest_client.egg-info/SOURCES.txt +1 -35
  16. {python_openhab_rest_client-21.4.0 → python_openhab_rest_client-21.4.1}/setup.py +1 -1
  17. python_openhab_rest_client-21.4.0/tests/Actions.py +0 -39
  18. python_openhab_rest_client-21.4.0/tests/Addons.py +0 -35
  19. python_openhab_rest_client-21.4.0/tests/Audio.py +0 -18
  20. python_openhab_rest_client-21.4.0/tests/Auth.py +0 -33
  21. python_openhab_rest_client-21.4.0/tests/ChannelEvents.py +0 -16
  22. python_openhab_rest_client-21.4.0/tests/ChannelTypes.py +0 -23
  23. python_openhab_rest_client-21.4.0/tests/ConfigDescriptions.py +0 -20
  24. python_openhab_rest_client-21.4.0/tests/Discovery.py +0 -20
  25. python_openhab_rest_client-21.4.0/tests/Events.py +0 -22
  26. python_openhab_rest_client-21.4.0/tests/Iconsets.py +0 -18
  27. python_openhab_rest_client-21.4.0/tests/Inbox.py +0 -26
  28. python_openhab_rest_client-21.4.0/tests/InboxEvents.py +0 -19
  29. python_openhab_rest_client-21.4.0/tests/ItemEvents.py +0 -28
  30. python_openhab_rest_client-21.4.0/tests/Items.py +0 -46
  31. python_openhab_rest_client-21.4.0/tests/LinkEvents.py +0 -20
  32. python_openhab_rest_client-21.4.0/tests/Links.py +0 -26
  33. python_openhab_rest_client-21.4.0/tests/Logging.py +0 -23
  34. python_openhab_rest_client-21.4.0/tests/ModuleTypes.py +0 -20
  35. python_openhab_rest_client-21.4.0/tests/Persistence.py +0 -32
  36. python_openhab_rest_client-21.4.0/tests/ProfileTypes.py +0 -16
  37. python_openhab_rest_client-21.4.0/tests/Rules.py +0 -38
  38. python_openhab_rest_client-21.4.0/tests/Services.py +0 -30
  39. python_openhab_rest_client-21.4.0/tests/Sitemaps.py +0 -25
  40. python_openhab_rest_client-21.4.0/tests/Systeminfo.py +0 -17
  41. python_openhab_rest_client-21.4.0/tests/Tags.py +0 -33
  42. python_openhab_rest_client-21.4.0/tests/Templates.py +0 -20
  43. python_openhab_rest_client-21.4.0/tests/ThingEvents.py +0 -23
  44. python_openhab_rest_client-21.4.0/tests/ThingTypes.py +0 -20
  45. python_openhab_rest_client-21.4.0/tests/Things.py +0 -34
  46. python_openhab_rest_client-21.4.0/tests/Transformations.py +0 -30
  47. python_openhab_rest_client-21.4.0/tests/UI.py +0 -45
  48. python_openhab_rest_client-21.4.0/tests/UUID.py +0 -16
  49. python_openhab_rest_client-21.4.0/tests/Voice.py +0 -45
  50. python_openhab_rest_client-21.4.0/tests/__init__.py +0 -0
  51. {python_openhab_rest_client-21.4.0 → python_openhab_rest_client-21.4.1}/LICENSE +0 -0
  52. {python_openhab_rest_client-21.4.0 → python_openhab_rest_client-21.4.1}/openhab/Actions.py +0 -0
  53. {python_openhab_rest_client-21.4.0 → python_openhab_rest_client-21.4.1}/openhab/Addons.py +0 -0
  54. {python_openhab_rest_client-21.4.0 → python_openhab_rest_client-21.4.1}/openhab/AsyncActions.py +0 -0
  55. {python_openhab_rest_client-21.4.0 → python_openhab_rest_client-21.4.1}/openhab/AsyncAddons.py +0 -0
  56. {python_openhab_rest_client-21.4.0 → python_openhab_rest_client-21.4.1}/openhab/AsyncAudio.py +0 -0
  57. {python_openhab_rest_client-21.4.0 → python_openhab_rest_client-21.4.1}/openhab/AsyncAuth.py +0 -0
  58. {python_openhab_rest_client-21.4.0 → python_openhab_rest_client-21.4.1}/openhab/AsyncChannelTypes.py +0 -0
  59. {python_openhab_rest_client-21.4.0 → python_openhab_rest_client-21.4.1}/openhab/AsyncClient.py +0 -0
  60. {python_openhab_rest_client-21.4.0 → python_openhab_rest_client-21.4.1}/openhab/AsyncConfigDescriptions.py +0 -0
  61. {python_openhab_rest_client-21.4.0 → python_openhab_rest_client-21.4.1}/openhab/AsyncDiscovery.py +0 -0
  62. {python_openhab_rest_client-21.4.0 → python_openhab_rest_client-21.4.1}/openhab/AsyncEvents.py +0 -0
  63. {python_openhab_rest_client-21.4.0 → python_openhab_rest_client-21.4.1}/openhab/AsyncIconsets.py +0 -0
  64. {python_openhab_rest_client-21.4.0 → python_openhab_rest_client-21.4.1}/openhab/AsyncInbox.py +0 -0
  65. {python_openhab_rest_client-21.4.0 → python_openhab_rest_client-21.4.1}/openhab/AsyncItems.py +0 -0
  66. {python_openhab_rest_client-21.4.0 → python_openhab_rest_client-21.4.1}/openhab/AsyncLogging.py +0 -0
  67. {python_openhab_rest_client-21.4.0 → python_openhab_rest_client-21.4.1}/openhab/AsyncModuleTypes.py +0 -0
  68. {python_openhab_rest_client-21.4.0 → python_openhab_rest_client-21.4.1}/openhab/AsyncProfileTypes.py +0 -0
  69. {python_openhab_rest_client-21.4.0 → python_openhab_rest_client-21.4.1}/openhab/AsyncServices.py +0 -0
  70. {python_openhab_rest_client-21.4.0 → python_openhab_rest_client-21.4.1}/openhab/AsyncSysteminfo.py +0 -0
  71. {python_openhab_rest_client-21.4.0 → python_openhab_rest_client-21.4.1}/openhab/AsyncTags.py +0 -0
  72. {python_openhab_rest_client-21.4.0 → python_openhab_rest_client-21.4.1}/openhab/AsyncTemplates.py +0 -0
  73. {python_openhab_rest_client-21.4.0 → python_openhab_rest_client-21.4.1}/openhab/AsyncThingTypes.py +0 -0
  74. {python_openhab_rest_client-21.4.0 → python_openhab_rest_client-21.4.1}/openhab/AsyncThings.py +0 -0
  75. {python_openhab_rest_client-21.4.0 → python_openhab_rest_client-21.4.1}/openhab/AsyncTransformations.py +0 -0
  76. {python_openhab_rest_client-21.4.0 → python_openhab_rest_client-21.4.1}/openhab/AsyncUI.py +0 -0
  77. {python_openhab_rest_client-21.4.0 → python_openhab_rest_client-21.4.1}/openhab/AsyncUUID.py +0 -0
  78. {python_openhab_rest_client-21.4.0 → python_openhab_rest_client-21.4.1}/openhab/Audio.py +0 -0
  79. {python_openhab_rest_client-21.4.0 → python_openhab_rest_client-21.4.1}/openhab/Auth.py +0 -0
  80. {python_openhab_rest_client-21.4.0 → python_openhab_rest_client-21.4.1}/openhab/ChannelTypes.py +0 -0
  81. {python_openhab_rest_client-21.4.0 → python_openhab_rest_client-21.4.1}/openhab/Client.py +0 -0
  82. {python_openhab_rest_client-21.4.0 → python_openhab_rest_client-21.4.1}/openhab/ConfigDescriptions.py +0 -0
  83. {python_openhab_rest_client-21.4.0 → python_openhab_rest_client-21.4.1}/openhab/Discovery.py +0 -0
  84. {python_openhab_rest_client-21.4.0 → python_openhab_rest_client-21.4.1}/openhab/Events.py +0 -0
  85. {python_openhab_rest_client-21.4.0 → python_openhab_rest_client-21.4.1}/openhab/Iconsets.py +0 -0
  86. {python_openhab_rest_client-21.4.0 → python_openhab_rest_client-21.4.1}/openhab/Inbox.py +0 -0
  87. {python_openhab_rest_client-21.4.0 → python_openhab_rest_client-21.4.1}/openhab/Items.py +0 -0
  88. {python_openhab_rest_client-21.4.0 → python_openhab_rest_client-21.4.1}/openhab/Logging.py +0 -0
  89. {python_openhab_rest_client-21.4.0 → python_openhab_rest_client-21.4.1}/openhab/ModuleTypes.py +0 -0
  90. {python_openhab_rest_client-21.4.0 → python_openhab_rest_client-21.4.1}/openhab/ProfileTypes.py +0 -0
  91. {python_openhab_rest_client-21.4.0 → python_openhab_rest_client-21.4.1}/openhab/SSE.py +0 -0
  92. {python_openhab_rest_client-21.4.0 → python_openhab_rest_client-21.4.1}/openhab/Services.py +0 -0
  93. {python_openhab_rest_client-21.4.0 → python_openhab_rest_client-21.4.1}/openhab/Systeminfo.py +0 -0
  94. {python_openhab_rest_client-21.4.0 → python_openhab_rest_client-21.4.1}/openhab/Tags.py +0 -0
  95. {python_openhab_rest_client-21.4.0 → python_openhab_rest_client-21.4.1}/openhab/Templates.py +0 -0
  96. {python_openhab_rest_client-21.4.0 → python_openhab_rest_client-21.4.1}/openhab/ThingTypes.py +0 -0
  97. {python_openhab_rest_client-21.4.0 → python_openhab_rest_client-21.4.1}/openhab/Things.py +0 -0
  98. {python_openhab_rest_client-21.4.0 → python_openhab_rest_client-21.4.1}/openhab/Transformations.py +0 -0
  99. {python_openhab_rest_client-21.4.0 → python_openhab_rest_client-21.4.1}/openhab/UI.py +0 -0
  100. {python_openhab_rest_client-21.4.0 → python_openhab_rest_client-21.4.1}/openhab/UUID.py +0 -0
  101. {python_openhab_rest_client-21.4.0 → python_openhab_rest_client-21.4.1}/openhab/tests/Actions.py +0 -0
  102. {python_openhab_rest_client-21.4.0 → python_openhab_rest_client-21.4.1}/openhab/tests/Addons.py +0 -0
  103. {python_openhab_rest_client-21.4.0 → python_openhab_rest_client-21.4.1}/openhab/tests/Audio.py +0 -0
  104. {python_openhab_rest_client-21.4.0 → python_openhab_rest_client-21.4.1}/openhab/tests/Auth.py +0 -0
  105. {python_openhab_rest_client-21.4.0 → python_openhab_rest_client-21.4.1}/openhab/tests/ChannelEvents.py +0 -0
  106. {python_openhab_rest_client-21.4.0 → python_openhab_rest_client-21.4.1}/openhab/tests/ChannelTypes.py +0 -0
  107. {python_openhab_rest_client-21.4.0 → python_openhab_rest_client-21.4.1}/openhab/tests/ConfigDescriptions.py +0 -0
  108. {python_openhab_rest_client-21.4.0 → python_openhab_rest_client-21.4.1}/openhab/tests/Discovery.py +0 -0
  109. {python_openhab_rest_client-21.4.0 → python_openhab_rest_client-21.4.1}/openhab/tests/Events.py +0 -0
  110. {python_openhab_rest_client-21.4.0 → python_openhab_rest_client-21.4.1}/openhab/tests/Iconsets.py +0 -0
  111. {python_openhab_rest_client-21.4.0 → python_openhab_rest_client-21.4.1}/openhab/tests/Inbox.py +0 -0
  112. {python_openhab_rest_client-21.4.0 → python_openhab_rest_client-21.4.1}/openhab/tests/InboxEvents.py +0 -0
  113. {python_openhab_rest_client-21.4.0 → python_openhab_rest_client-21.4.1}/openhab/tests/ItemEvents.py +0 -0
  114. {python_openhab_rest_client-21.4.0 → python_openhab_rest_client-21.4.1}/openhab/tests/Items.py +0 -0
  115. {python_openhab_rest_client-21.4.0 → python_openhab_rest_client-21.4.1}/openhab/tests/LinkEvents.py +0 -0
  116. {python_openhab_rest_client-21.4.0 → python_openhab_rest_client-21.4.1}/openhab/tests/Links.py +0 -0
  117. {python_openhab_rest_client-21.4.0 → python_openhab_rest_client-21.4.1}/openhab/tests/Logging.py +0 -0
  118. {python_openhab_rest_client-21.4.0 → python_openhab_rest_client-21.4.1}/openhab/tests/ModuleTypes.py +0 -0
  119. {python_openhab_rest_client-21.4.0 → python_openhab_rest_client-21.4.1}/openhab/tests/Persistence.py +0 -0
  120. {python_openhab_rest_client-21.4.0 → python_openhab_rest_client-21.4.1}/openhab/tests/ProfileTypes.py +0 -0
  121. {python_openhab_rest_client-21.4.0 → python_openhab_rest_client-21.4.1}/openhab/tests/Rules.py +0 -0
  122. {python_openhab_rest_client-21.4.0 → python_openhab_rest_client-21.4.1}/openhab/tests/Services.py +0 -0
  123. {python_openhab_rest_client-21.4.0 → python_openhab_rest_client-21.4.1}/openhab/tests/Sitemaps.py +0 -0
  124. {python_openhab_rest_client-21.4.0 → python_openhab_rest_client-21.4.1}/openhab/tests/Systeminfo.py +0 -0
  125. {python_openhab_rest_client-21.4.0 → python_openhab_rest_client-21.4.1}/openhab/tests/Tags.py +0 -0
  126. {python_openhab_rest_client-21.4.0 → python_openhab_rest_client-21.4.1}/openhab/tests/Templates.py +0 -0
  127. {python_openhab_rest_client-21.4.0 → python_openhab_rest_client-21.4.1}/openhab/tests/ThingEvents.py +0 -0
  128. {python_openhab_rest_client-21.4.0 → python_openhab_rest_client-21.4.1}/openhab/tests/ThingTypes.py +0 -0
  129. {python_openhab_rest_client-21.4.0 → python_openhab_rest_client-21.4.1}/openhab/tests/Things.py +0 -0
  130. {python_openhab_rest_client-21.4.0 → python_openhab_rest_client-21.4.1}/openhab/tests/Transformations.py +0 -0
  131. {python_openhab_rest_client-21.4.0 → python_openhab_rest_client-21.4.1}/openhab/tests/UI.py +0 -0
  132. {python_openhab_rest_client-21.4.0 → python_openhab_rest_client-21.4.1}/openhab/tests/UUID.py +0 -0
  133. {python_openhab_rest_client-21.4.0 → python_openhab_rest_client-21.4.1}/openhab/tests/Voice.py +0 -0
  134. {python_openhab_rest_client-21.4.0 → python_openhab_rest_client-21.4.1}/openhab/tests/__init__.py +0 -0
  135. {python_openhab_rest_client-21.4.0 → python_openhab_rest_client-21.4.1}/pyproject.toml +0 -0
  136. {python_openhab_rest_client-21.4.0 → python_openhab_rest_client-21.4.1}/python_openhab_rest_client.egg-info/dependency_links.txt +0 -0
  137. {python_openhab_rest_client-21.4.0 → python_openhab_rest_client-21.4.1}/python_openhab_rest_client.egg-info/requires.txt +0 -0
  138. {python_openhab_rest_client-21.4.0 → python_openhab_rest_client-21.4.1}/python_openhab_rest_client.egg-info/top_level.txt +0 -0
  139. {python_openhab_rest_client-21.4.0 → python_openhab_rest_client-21.4.1}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: python-openhab-rest-client
3
- Version: 21.4.0
3
+ Version: 21.4.1
4
4
  Summary: A Python client for the openHAB REST API. This library enables easy interaction with the openHAB REST API to control smart home devices, retrieve status information, and process events.
5
5
  Home-page: https://github.com/Michdo93/python-openhab-rest-client
6
6
  Author: Michael Christian Dörflinger
@@ -84,6 +84,76 @@ Install via pip:
84
84
  pip install python-openhab-rest-client
85
85
  ```
86
86
 
87
+ Here is the complete section translated into English, formatted to match your README:
88
+
89
+ ---
90
+
91
+ ### Secure HTTPS & Self-Signed Certificates (TLS)
92
+
93
+ When running openHAB over HTTPS with a custom or self-signed certificate, you must ensure that Python (via the underlying `requests` library) trusts the certificate and successfully validates the hostname.
94
+
95
+ #### 1. Switch openHAB to HTTPS
96
+
97
+ 1. **Disable HTTP:**
98
+ Open the configuration file `/etc/openhab/services/org.ops4j.pax.web.cfg` (if it is not existing, create it) and set:
99
+ ```properties
100
+ org.osgi.service.http.enabled=false
101
+ org.osgi.service.http.secure.enabled=true
102
+ org.ops4j.pax.web.ssl.port=8443
103
+ ```
104
+
105
+ It could also help to edit the `/etc/default/openhab` file and make this changes:
106
+
107
+ ```
108
+ #########################
109
+ ## PORTS
110
+ ## The ports openHAB will bind its HTTP/HTTPS web server to.
111
+
112
+ #OPENHAB_HTTP_PORT=8080
113
+ OPENHAB_HTTPS_PORT=8443
114
+ ```
115
+
116
+ This ensures openHAB runs exclusively on the secure HTTPS port (default `8443`).
117
+ 2. **Generate Keystore with a Proper SAN (Subject Alternative Name):**
118
+ Modern OpenSSL and Python versions strictly require a SAN entry in the certificate to prevent hostname mismatches. Create a new keystore using Java's `keytool` (replace `<your-hostname>` with your actual hostname):
119
+ ```bash
120
+ sudo rm /var/lib/openhab/etc/keystore
121
+ sudo keytool -genkeypair -alias openhab -keyalg RSA -keysize 2048 -validity 365 \
122
+ -keystore /var/lib/openhab/etc/keystore -storepass openhab -keypass openhab \
123
+ -dname "CN=openhab" -ext "san=dns:<your-hostname>,ip:127.0.0.1,dns:localhost"
124
+ sudo chown openhab:openhab /var/lib/openhab/etc/keystore
125
+ sudo systemctl restart openhab
126
+ ```
127
+
128
+ 3. **Export the Certificate in PEM Format:**
129
+ Export the public certificate so your Python client can read and trust it:
130
+ ```bash
131
+ sudo keytool -exportcert -rfc -alias openhab -keystore /var/lib/openhab/etc/keystore \
132
+ -storepass openhab -file /home/$USER/openhab-exported.crt
133
+ sudo chown $USER:$USER /home/$USER/openhab-exported.crt
134
+ ```
135
+
136
+ #### 2. Usage in the Python Client
137
+
138
+ Since self-signed certificates are rejected by default, you can pass the absolute path to the exported `.crt` file via the `verify` parameter of the client. The library will cryptographically verify the connection against your certificate:
139
+
140
+ ```python
141
+ from openhab import OpenHABClient, Items
142
+
143
+ client = OpenHABClient(
144
+ url="https://<your-hostname>:8443",
145
+ username="openhab",
146
+ password="habopen",
147
+ verify="/home/smarthome/openhab-exported.crt"
148
+ )
149
+
150
+ itemsAPI = Items(client)
151
+ allItems = itemsAPI.getItems()
152
+ print("All Items:", allItems)
153
+ ```
154
+
155
+ *(Note: For the asynchronous client `AsyncOpenHABClient`, the `verify` parameter works the exact same way via `aiohttp`.)*
156
+
87
157
  ## Usage
88
158
 
89
159
  Basically, you always need the `OpenHABClient` (or `AsyncOpenHABClient` for async usage). Regardless of whether you carry out a normal `REST request` or one via `SSE`.
@@ -3009,6 +3079,27 @@ This project is licensed under the MIT License. See the [LICENSE](LICENSE) file
3009
3079
 
3010
3080
  ## Changelog
3011
3081
 
3082
+ ### 21.4.1
3083
+
3084
+ Bug fixes (found while writing the examples of the book »openHAB & Python-Entwicklung«
3085
+ against openHAB 5.2.3):
3086
+
3087
+ - **`Sitemaps.getSitemapEvents()` / `getFullSitemapEvents()`** read the SSE endpoints like a
3088
+ normal response and failed after 10 s with "Read timed out". They now open the stream via
3089
+ `executeSSE()` and return it (read it with `iterSSELines()` and `parseSSEData()` – sitemap
3090
+ events have no `topic`). Note: bind the subscription to a page first with
3091
+ `getSitemapPage(sitemap, page, subscriptionID=...)`; openHAB answers the stream request only
3092
+ when the first event arrives.
3093
+ - **`Rules.setModuleConfigParam()`** sent the value with `json.dumps()`, so `10` was stored as
3094
+ `"10"` (with quotes) and e.g. an `ItemCommandAction` sent an invalid command. The value is now
3095
+ sent as plain text.
3096
+ - **`Links.deleteAllLinks()`** failed with `500 No message body writer` because of the missing
3097
+ `Accept: application/json` header. It now returns openHAB's answer, e.g. `{"count": 1}`.
3098
+ - **`Persistence.deleteItemData()`** reported every `400` as "Invalid filter parameters.";
3099
+ openHAB also uses `400` for read-only services such as rrd4j. openHAB's reason is appended now.
3100
+ - **`Voice.interpretText()` / `interpretTextBatch()`** dropped the interpreter's answer on `400`
3101
+ (e.g. "Entschuldigung, aber das habe ich nicht verstanden."). It is appended now.
3102
+
3012
3103
  ### 21.4.0
3013
3104
 
3014
3105
  Bug fixes (all found against openHAB 5.2.3):
@@ -57,6 +57,76 @@ Install via pip:
57
57
  pip install python-openhab-rest-client
58
58
  ```
59
59
 
60
+ Here is the complete section translated into English, formatted to match your README:
61
+
62
+ ---
63
+
64
+ ### Secure HTTPS & Self-Signed Certificates (TLS)
65
+
66
+ When running openHAB over HTTPS with a custom or self-signed certificate, you must ensure that Python (via the underlying `requests` library) trusts the certificate and successfully validates the hostname.
67
+
68
+ #### 1. Switch openHAB to HTTPS
69
+
70
+ 1. **Disable HTTP:**
71
+ Open the configuration file `/etc/openhab/services/org.ops4j.pax.web.cfg` (if it is not existing, create it) and set:
72
+ ```properties
73
+ org.osgi.service.http.enabled=false
74
+ org.osgi.service.http.secure.enabled=true
75
+ org.ops4j.pax.web.ssl.port=8443
76
+ ```
77
+
78
+ It could also help to edit the `/etc/default/openhab` file and make this changes:
79
+
80
+ ```
81
+ #########################
82
+ ## PORTS
83
+ ## The ports openHAB will bind its HTTP/HTTPS web server to.
84
+
85
+ #OPENHAB_HTTP_PORT=8080
86
+ OPENHAB_HTTPS_PORT=8443
87
+ ```
88
+
89
+ This ensures openHAB runs exclusively on the secure HTTPS port (default `8443`).
90
+ 2. **Generate Keystore with a Proper SAN (Subject Alternative Name):**
91
+ Modern OpenSSL and Python versions strictly require a SAN entry in the certificate to prevent hostname mismatches. Create a new keystore using Java's `keytool` (replace `<your-hostname>` with your actual hostname):
92
+ ```bash
93
+ sudo rm /var/lib/openhab/etc/keystore
94
+ sudo keytool -genkeypair -alias openhab -keyalg RSA -keysize 2048 -validity 365 \
95
+ -keystore /var/lib/openhab/etc/keystore -storepass openhab -keypass openhab \
96
+ -dname "CN=openhab" -ext "san=dns:<your-hostname>,ip:127.0.0.1,dns:localhost"
97
+ sudo chown openhab:openhab /var/lib/openhab/etc/keystore
98
+ sudo systemctl restart openhab
99
+ ```
100
+
101
+ 3. **Export the Certificate in PEM Format:**
102
+ Export the public certificate so your Python client can read and trust it:
103
+ ```bash
104
+ sudo keytool -exportcert -rfc -alias openhab -keystore /var/lib/openhab/etc/keystore \
105
+ -storepass openhab -file /home/$USER/openhab-exported.crt
106
+ sudo chown $USER:$USER /home/$USER/openhab-exported.crt
107
+ ```
108
+
109
+ #### 2. Usage in the Python Client
110
+
111
+ Since self-signed certificates are rejected by default, you can pass the absolute path to the exported `.crt` file via the `verify` parameter of the client. The library will cryptographically verify the connection against your certificate:
112
+
113
+ ```python
114
+ from openhab import OpenHABClient, Items
115
+
116
+ client = OpenHABClient(
117
+ url="https://<your-hostname>:8443",
118
+ username="openhab",
119
+ password="habopen",
120
+ verify="/home/smarthome/openhab-exported.crt"
121
+ )
122
+
123
+ itemsAPI = Items(client)
124
+ allItems = itemsAPI.getItems()
125
+ print("All Items:", allItems)
126
+ ```
127
+
128
+ *(Note: For the asynchronous client `AsyncOpenHABClient`, the `verify` parameter works the exact same way via `aiohttp`.)*
129
+
60
130
  ## Usage
61
131
 
62
132
  Basically, you always need the `OpenHABClient` (or `AsyncOpenHABClient` for async usage). Regardless of whether you carry out a normal `REST request` or one via `SSE`.
@@ -2982,6 +3052,27 @@ This project is licensed under the MIT License. See the [LICENSE](LICENSE) file
2982
3052
 
2983
3053
  ## Changelog
2984
3054
 
3055
+ ### 21.4.1
3056
+
3057
+ Bug fixes (found while writing the examples of the book »openHAB & Python-Entwicklung«
3058
+ against openHAB 5.2.3):
3059
+
3060
+ - **`Sitemaps.getSitemapEvents()` / `getFullSitemapEvents()`** read the SSE endpoints like a
3061
+ normal response and failed after 10 s with "Read timed out". They now open the stream via
3062
+ `executeSSE()` and return it (read it with `iterSSELines()` and `parseSSEData()` – sitemap
3063
+ events have no `topic`). Note: bind the subscription to a page first with
3064
+ `getSitemapPage(sitemap, page, subscriptionID=...)`; openHAB answers the stream request only
3065
+ when the first event arrives.
3066
+ - **`Rules.setModuleConfigParam()`** sent the value with `json.dumps()`, so `10` was stored as
3067
+ `"10"` (with quotes) and e.g. an `ItemCommandAction` sent an invalid command. The value is now
3068
+ sent as plain text.
3069
+ - **`Links.deleteAllLinks()`** failed with `500 No message body writer` because of the missing
3070
+ `Accept: application/json` header. It now returns openHAB's answer, e.g. `{"count": 1}`.
3071
+ - **`Persistence.deleteItemData()`** reported every `400` as "Invalid filter parameters.";
3072
+ openHAB also uses `400` for read-only services such as rrd4j. openHAB's reason is appended now.
3073
+ - **`Voice.interpretText()` / `interpretTextBatch()`** dropped the interpreter's answer on `400`
3074
+ (e.g. "Entschuldigung, aber das habe ich nicht verstanden."). It is appended now.
3075
+
2985
3076
  ### 21.4.0
2986
3077
 
2987
3078
  Bug fixes (all found against openHAB 5.2.3):
@@ -185,7 +185,8 @@ class AsyncLinks:
185
185
  :return: The API response when all links are successfully deleted.
186
186
  """
187
187
  try:
188
- response = await self.client.delete(f"/links/{object}")
188
+ response = await self.client.delete(f"/links/{object}",
189
+ headers={"Accept": "application/json"})
189
190
 
190
191
  if isinstance(response, StatusResponse):
191
192
  status_code = response["status"]
@@ -308,7 +308,8 @@ class AsyncPersistence:
308
308
  except aiohttp.ClientResponseError as err:
309
309
  status_code = err.status
310
310
  if status_code == 400:
311
- return {"error": "Invalid filter parameters."}
311
+ # openHAB uses 400 for invalid filters and for read-only services (e.g. rrd4j)
312
+ return {"error": f"Invalid filter parameters or service not modifiable: {str(err)}"}
312
313
  elif status_code == 404:
313
314
  return {"error": "Unknown persistence service."}
314
315
  else:
@@ -315,7 +315,7 @@ class AsyncRules:
315
315
  """
316
316
  try:
317
317
  response = await self.client.put(f"/rules/{ruleUID}/{moduleCategory}/{moduleID}/config/{param}",
318
- data=json.dumps(value), headers={'Content-Type': 'text/plain'})
318
+ data=str(value), headers={"Content-Type": "text/plain"})
319
319
 
320
320
  if isinstance(response, StatusResponse):
321
321
  status_code = response["status"]
@@ -14,6 +14,18 @@ class AsyncSitemaps:
14
14
  """
15
15
  self.client = client
16
16
 
17
+ async def _openEventStream(self, path: str, params: dict, errors: dict):
18
+ """Opens a sitemap SSE stream; returns the streaming response or {"error": ...}."""
19
+ try:
20
+ return await self.client.executeSSE(path, params=params)
21
+ except aiohttp.ClientResponseError as err:
22
+ statusCode = err.status
23
+ if statusCode in errors:
24
+ return {"error": errors[statusCode]}
25
+ return {"error": f"HTTP error {statusCode}: {str(err)}"}
26
+ except (aiohttp.ClientError, asyncio.TimeoutError) as err:
27
+ return {"error": f"Request error: {str(err)}"}
28
+
17
29
  async def getSitemaps(self):
18
30
  """
19
31
  Get all available sitemaps.
@@ -175,82 +187,33 @@ class AsyncSitemaps:
175
187
 
176
188
  async def getSitemapEvents(self, subscriptionID: str, sitemapName: str = None, pageID: str = None):
177
189
  """
178
- Get sitemap events.
190
+ Opens the event stream (SSE) of a sitemap subscription for one page.
179
191
 
180
- :param subscriptionID: The ID of the subscription.
181
- :param sitemap: The name of the sitemap (optional).
182
- :param pageID: The ID of the page (optional).
192
+ :param subscriptionID: The ID of the subscription (see subscribeToSitemapEvents()).
193
+ :param sitemapName: The name of the sitemap.
194
+ :param pageID: The ID of the page (the sitemap name for the home page).
183
195
 
184
- :return: The events (JSON).
196
+ :return: A SSE stream (aiohttp.ClientResponse, read it with iterSSEEvents()) or {"error": ...}.
185
197
  """
186
- try:
187
- response = await self.client.get(
188
- f"/sitemaps/events/{subscriptionID}", params={"sitemap": sitemapName, "pageid": pageID})
189
-
190
- if isinstance(response, StatusResponse):
191
- status_code = response["status"]
192
- else:
193
- return response
194
-
195
- except aiohttp.ClientResponseError as err:
196
- status_code = err.status
197
- if status_code == 400:
198
- return {"error": "Missing sitemap or page parameter, or page not linked successfully to the subscription."}
199
- elif status_code == 404:
200
- return {"error": "Subscription not found."}
201
- else:
202
- return {"error": f"HTTP error {status_code}: {str(err)}"}
203
-
204
- except (aiohttp.ClientError, asyncio.TimeoutError) as err:
205
- return {"error": f"Request error: {str(err)}"}
206
-
207
- if status_code == 200:
208
- return {"message": "OK"}
209
- elif status_code == 404:
210
- return {"error": "Subscription not found."}
211
- elif status_code == 400:
212
- return {"error": "Missing sitemap or page parameter, or page not linked successfully to the subscription."}
213
-
214
- return {"error": f"Unexpected response: {status_code}"}
198
+ return await self._openEventStream(
199
+ f"/rest/sitemaps/events/{subscriptionID}", {"sitemap": sitemapName, "pageid": pageID},
200
+ {400: "Missing sitemap or page parameter, or page not linked successfully to the subscription.",
201
+ 404: "Subscription not found."})
215
202
 
216
203
  async def getFullSitemapEvents(self, subscriptionID: str, sitemapName: str = None):
217
204
  """
218
- Get sitemap events for a whole sitemap. Not recommended due to potentially high traffic.
205
+ Opens the event stream (SSE) of a sitemap subscription for the whole sitemap.
206
+ Not recommended due to potentially high traffic.
219
207
 
220
208
  :param subscriptionID: The ID of the subscription.
221
- :param sitemap: The name of the sitemap (optional).
209
+ :param sitemapName: The name of the sitemap.
222
210
 
223
- :return: The events for the entire sitemap (JSON).
211
+ :return: A SSE stream (aiohttp.ClientResponse) or {"error": ...}.
224
212
  """
225
- try:
226
- response = await self.client.get(
227
- f"/sitemaps/events/{subscriptionID}/*", params={"sitemap": sitemapName})
228
-
229
- if isinstance(response, StatusResponse):
230
- status_code = response["status"]
231
- else:
232
- return response
233
-
234
- except aiohttp.ClientResponseError as err:
235
- status_code = err.status
236
- if status_code == 400:
237
- return {"error": "Missing sitemap parameter, or sitemap not linked successfully to the subscription."}
238
- elif status_code == 404:
239
- return {"error": "Subscription not found."}
240
- else:
241
- return {"error": f"HTTP error {status_code}: {str(err)}"}
242
-
243
- except (aiohttp.ClientError, asyncio.TimeoutError) as err:
244
- return {"error": f"Request error: {str(err)}"}
245
-
246
- if status_code == 200:
247
- return {"message": "OK"}
248
- elif status_code == 404:
249
- return {"error": "Subscription not found."}
250
- elif status_code == 400:
251
- return {"error": "Missing sitemap parameter, or sitemap not linked successfully to the subscription."}
252
-
253
- return {"error": f"Unexpected response: {status_code}"}
213
+ return await self._openEventStream(
214
+ f"/rest/sitemaps/events/{subscriptionID}/*", {"sitemap": sitemapName},
215
+ {400: "Missing sitemap parameter, or sitemap not linked successfully to the subscription.",
216
+ 404: "Subscription not found."})
254
217
 
255
218
  async def subscribeToSitemapEvents(self):
256
219
  """
@@ -190,7 +190,8 @@ class AsyncVoice:
190
190
  except aiohttp.ClientResponseError as err:
191
191
  status_code = err.status
192
192
  if status_code == 400:
193
- return {"error": "Interpretation exception occurs."}
193
+ # openHAB explains in its error message why the text was not understood
194
+ return {"error": f"Interpretation exception occurs: {str(err)}"}
194
195
  elif status_code == 404:
195
196
  return {"error": "No human language interpreter was found."}
196
197
  else:
@@ -272,7 +273,8 @@ class AsyncVoice:
272
273
  except aiohttp.ClientResponseError as err:
273
274
  status_code = err.status
274
275
  if status_code == 400:
275
- return {"error": "Interpretation exception occurs."}
276
+ # openHAB explains in its error message why the text was not understood
277
+ return {"error": f"Interpretation exception occurs: {str(err)}"}
276
278
  elif status_code == 404:
277
279
  return {"error": "No human language interpreter was found."}
278
280
  else:
@@ -182,7 +182,8 @@ class Links:
182
182
  :return: The API response when all links are successfully deleted.
183
183
  """
184
184
  try:
185
- response = self.client.delete(f"/links/{object}")
185
+ response = self.client.delete(f"/links/{object}",
186
+ headers={"Accept": "application/json"})
186
187
 
187
188
  if isinstance(response, StatusResponse):
188
189
  status_code = response["status"]
@@ -305,7 +305,8 @@ class Persistence:
305
305
  except requests.exceptions.HTTPError as err:
306
306
  status_code = err.response.status_code
307
307
  if status_code == 400:
308
- return {"error": "Invalid filter parameters."}
308
+ # openHAB uses 400 for invalid filters and for read-only services (e.g. rrd4j)
309
+ return {"error": f"Invalid filter parameters or service not modifiable: {str(err)}"}
309
310
  elif status_code == 404:
310
311
  return {"error": "Unknown persistence service."}
311
312
  else:
@@ -312,7 +312,7 @@ class Rules:
312
312
  """
313
313
  try:
314
314
  response = self.client.put(f"/rules/{ruleUID}/{moduleCategory}/{moduleID}/config/{param}",
315
- data=json.dumps(value), headers={'Content-Type': 'text/plain'})
315
+ data=str(value), headers={"Content-Type": "text/plain"})
316
316
 
317
317
  if isinstance(response, StatusResponse):
318
318
  status_code = response["status"]
@@ -11,6 +11,18 @@ class Sitemaps:
11
11
  """
12
12
  self.client = client
13
13
 
14
+ def _openEventStream(self, path: str, params: dict, errors: dict):
15
+ """Opens a sitemap SSE stream; returns the streaming response or {"error": ...}."""
16
+ try:
17
+ return self.client.executeSSE(path, params=params)
18
+ except requests.exceptions.HTTPError as err:
19
+ statusCode = err.response.status_code
20
+ if statusCode in errors:
21
+ return {"error": errors[statusCode]}
22
+ return {"error": f"HTTP error {statusCode}: {str(err)}"}
23
+ except requests.exceptions.RequestException as err:
24
+ return {"error": f"Request error: {str(err)}"}
25
+
14
26
  def getSitemaps(self):
15
27
  """
16
28
  Get all available sitemaps.
@@ -172,82 +184,33 @@ class Sitemaps:
172
184
 
173
185
  def getSitemapEvents(self, subscriptionID: str, sitemapName: str = None, pageID: str = None):
174
186
  """
175
- Get sitemap events.
187
+ Opens the event stream (SSE) of a sitemap subscription for one page.
176
188
 
177
- :param subscriptionID: The ID of the subscription.
178
- :param sitemap: The name of the sitemap (optional).
179
- :param pageID: The ID of the page (optional).
189
+ :param subscriptionID: The ID of the subscription (see subscribeToSitemapEvents()).
190
+ :param sitemapName: The name of the sitemap.
191
+ :param pageID: The ID of the page (the sitemap name for the home page).
180
192
 
181
- :return: The events (JSON).
193
+ :return: A SSE stream (requests.Response, read it with iterSSEEvents()) or {"error": ...}.
182
194
  """
183
- try:
184
- response = self.client.get(
185
- f"/sitemaps/events/{subscriptionID}", params={"sitemap": sitemapName, "pageid": pageID})
186
-
187
- if isinstance(response, StatusResponse):
188
- status_code = response["status"]
189
- else:
190
- return response
191
-
192
- except requests.exceptions.HTTPError as err:
193
- status_code = err.response.status_code
194
- if status_code == 400:
195
- return {"error": "Missing sitemap or page parameter, or page not linked successfully to the subscription."}
196
- elif status_code == 404:
197
- return {"error": "Subscription not found."}
198
- else:
199
- return {"error": f"HTTP error {status_code}: {str(err)}"}
200
-
201
- except requests.exceptions.RequestException as err:
202
- return {"error": f"Request error: {str(err)}"}
203
-
204
- if status_code == 200:
205
- return {"message": "OK"}
206
- elif status_code == 404:
207
- return {"error": "Subscription not found."}
208
- elif status_code == 400:
209
- return {"error": "Missing sitemap or page parameter, or page not linked successfully to the subscription."}
210
-
211
- return {"error": f"Unexpected response: {status_code}"}
195
+ return self._openEventStream(
196
+ f"/rest/sitemaps/events/{subscriptionID}", {"sitemap": sitemapName, "pageid": pageID},
197
+ {400: "Missing sitemap or page parameter, or page not linked successfully to the subscription.",
198
+ 404: "Subscription not found."})
212
199
 
213
200
  def getFullSitemapEvents(self, subscriptionID: str, sitemapName: str = None):
214
201
  """
215
- Get sitemap events for a whole sitemap. Not recommended due to potentially high traffic.
202
+ Opens the event stream (SSE) of a sitemap subscription for the whole sitemap.
203
+ Not recommended due to potentially high traffic.
216
204
 
217
205
  :param subscriptionID: The ID of the subscription.
218
- :param sitemap: The name of the sitemap (optional).
206
+ :param sitemapName: The name of the sitemap.
219
207
 
220
- :return: The events for the entire sitemap (JSON).
208
+ :return: A SSE stream (requests.Response) or {"error": ...}.
221
209
  """
222
- try:
223
- response = self.client.get(
224
- f"/sitemaps/events/{subscriptionID}/*", params={"sitemap": sitemapName})
225
-
226
- if isinstance(response, StatusResponse):
227
- status_code = response["status"]
228
- else:
229
- return response
230
-
231
- except requests.exceptions.HTTPError as err:
232
- status_code = err.response.status_code
233
- if status_code == 400:
234
- return {"error": "Missing sitemap parameter, or sitemap not linked successfully to the subscription."}
235
- elif status_code == 404:
236
- return {"error": "Subscription not found."}
237
- else:
238
- return {"error": f"HTTP error {status_code}: {str(err)}"}
239
-
240
- except requests.exceptions.RequestException as err:
241
- return {"error": f"Request error: {str(err)}"}
242
-
243
- if status_code == 200:
244
- return {"message": "OK"}
245
- elif status_code == 404:
246
- return {"error": "Subscription not found."}
247
- elif status_code == 400:
248
- return {"error": "Missing sitemap parameter, or sitemap not linked successfully to the subscription."}
249
-
250
- return {"error": f"Unexpected response: {status_code}"}
210
+ return self._openEventStream(
211
+ f"/rest/sitemaps/events/{subscriptionID}/*", {"sitemap": sitemapName},
212
+ {400: "Missing sitemap parameter, or sitemap not linked successfully to the subscription.",
213
+ 404: "Subscription not found."})
251
214
 
252
215
  def subscribeToSitemapEvents(self):
253
216
  """
@@ -187,7 +187,8 @@ class Voice:
187
187
  except requests.exceptions.HTTPError as err:
188
188
  status_code = err.response.status_code
189
189
  if status_code == 400:
190
- return {"error": "Interpretation exception occurs."}
190
+ # openHAB explains in its error message why the text was not understood
191
+ return {"error": f"Interpretation exception occurs: {str(err)}"}
191
192
  elif status_code == 404:
192
193
  return {"error": "No human language interpreter was found."}
193
194
  else:
@@ -269,7 +270,8 @@ class Voice:
269
270
  except requests.exceptions.HTTPError as err:
270
271
  status_code = err.response.status_code
271
272
  if status_code == 400:
272
- return {"error": "Interpretation exception occurs."}
273
+ # openHAB explains in its error message why the text was not understood
274
+ return {"error": f"Interpretation exception occurs: {str(err)}"}
273
275
  elif status_code == 404:
274
276
  return {"error": "No human language interpreter was found."}
275
277
  else:
@@ -1,4 +1,4 @@
1
- __version__ = "21.4.0"
1
+ __version__ = "21.4.1"
2
2
 
3
3
  from .Actions import Actions
4
4
  from .Addons import Addons