forked from irgusite/hass-Duet3D
-
Notifications
You must be signed in to change notification settings - Fork 0
/
__init__.py
259 lines (225 loc) · 9.82 KB
/
__init__.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
"""Support for monitoring Duet 3D printers."""
import logging
import time
import requests
import voluptuous as vol
from aiohttp.hdrs import CONTENT_TYPE
from homeassistant.components.discovery import SERVICE_OCTOPRINT
from homeassistant.const import (
CONF_API_KEY, CONF_HOST, CONTENT_TYPE_JSON, CONF_NAME, CONF_PATH,
CONF_PORT, CONF_SSL, TEMP_CELSIUS, CONF_MONITORED_CONDITIONS, CONF_SENSORS,
CONF_BINARY_SENSORS)
from homeassistant.helpers import discovery
import homeassistant.helpers.config_validation as cv
from homeassistant.helpers.discovery import load_platform
from homeassistant.util import slugify as util_slugify
_LOGGER = logging.getLogger(__name__)
CONF_BED = 'bed'
CONF_NUMBER_OF_TOOLS = 'number_of_tools'
DEFAULT_NAME = 'Duet3d'
DOMAIN = 'duet3d'
def has_all_unique_names(value):
"""Validate that printers have an unique name."""
names = [util_slugify(printer['name']) for printer in value]
vol.Schema(vol.Unique())(names)
return value
def ensure_valid_path(value):
"""Validate the path, ensuring it starts and ends with a /."""
vol.Schema(cv.string)(value)
if value[0] != '/':
value = '/' + value
if value[-1] != '/':
value += '/'
return value
BINARY_SENSOR_TYPES = {
# API Endpoint, Group, Key, unit
'Printing': ['job', 'status', 'printing', None],
#"Printing Error": ['printer', 'state', 'error', None]
}
BINARY_SENSOR_SCHEMA = vol.Schema({
vol.Optional(CONF_MONITORED_CONDITIONS, default=list(BINARY_SENSOR_TYPES)):
vol.All(cv.ensure_list, [vol.In(BINARY_SENSOR_TYPES)]),
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
})
SENSOR_TYPES = {
# API Endpoint, Group, Key, unit, icon
# Group, subgroup, key, unit, icon
'Temperatures': ['temps', 'temperature', '*', TEMP_CELSIUS],
"Current State": ['job', 'status', 'text', None, 'mdi:printer-3d'],
"Job Percentage": ['job', 'progress', 'completion', '%',
'mdi:file-percent'],
"Time Remaining": ['job', 'timesLeft', 'file', 'seconds',
'mdi:clock-end'],
"Time Elapsed": ['job', 'printDuration', 'printTime', 'seconds',
'mdi:clock-start'],
}
SENSOR_SCHEMA = vol.Schema({
vol.Optional(CONF_MONITORED_CONDITIONS, default=list(SENSOR_TYPES)):
vol.All(cv.ensure_list, [vol.In(SENSOR_TYPES)]),
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
})
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.All(cv.ensure_list, [vol.Schema({
#vol.Required(CONF_API_KEY): cv.string,
vol.Required(CONF_HOST): cv.string,
vol.Optional(CONF_SSL, default=False): cv.boolean,
vol.Optional(CONF_PORT, default=80): cv.port,
# type 2, extended infos, type 3, print status infos
vol.Optional(CONF_PATH, default='/rr_status?type=3'): ensure_valid_path,
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
vol.Optional(CONF_NUMBER_OF_TOOLS, default=0): cv.positive_int,
vol.Optional(CONF_BED, default=False): cv.boolean,
vol.Optional(CONF_SENSORS, default={}): SENSOR_SCHEMA,
vol.Optional(CONF_BINARY_SENSORS, default={}): BINARY_SENSOR_SCHEMA
})], has_all_unique_names),
}, extra=vol.ALLOW_EXTRA)
def setup(hass, config):
"""Set up the OctoPrint component."""
printers = hass.data[DOMAIN] = {}
success = False
def device_discovered(service, info):
"""Get called when an Octoprint server has been discovered."""
_LOGGER.debug("Found an Octoprint server: %s", info)
discovery.async_listen(hass, SERVICE_OCTOPRINT, device_discovered)
if DOMAIN not in config:
# Skip the setup if there is no configuration present
return True
for printer in config[DOMAIN]:
name = printer[CONF_NAME]
ssl = 's' if printer[CONF_SSL] else ''
base_url = 'http{}://{}:{}{}api/'.format(ssl,
printer[CONF_HOST],
printer[CONF_PORT],
printer[CONF_PATH])
api_key = 0
number_of_tools = printer[CONF_NUMBER_OF_TOOLS]
bed = printer[CONF_BED]
try:
octoprint_api = Duet3dAPI(base_url, api_key, bed,
number_of_tools)
printers[base_url] = octoprint_api
octoprint_api.get('printer')
octoprint_api.get('job')
except requests.exceptions.RequestException as conn_err:
_LOGGER.error("Error setting up OctoPrint API: %r", conn_err)
continue
sensors = printer[CONF_SENSORS][CONF_MONITORED_CONDITIONS]
load_platform(hass, 'sensor', DOMAIN, {'name': name,
'base_url': base_url,
'sensors': sensors}, config)
b_sensors = printer[CONF_BINARY_SENSORS][CONF_MONITORED_CONDITIONS]
load_platform(hass, 'binary_sensor', DOMAIN, {'name': name,
'base_url': base_url,
'sensors': b_sensors},
config)
success = True
return success
class Duet3dAPI:
"""Simple JSON wrapper for OctoPrint's API."""
def __init__(self, api_url, key, bed, number_of_tools):
"""Initialize OctoPrint API and set headers needed later."""
self.api_url = api_url
self.headers = {
CONTENT_TYPE: CONTENT_TYPE_JSON,
#'X-Api-Key': key,
}
self.printer_last_reading = [{}, None]
self.job_last_reading = [{}, None]
self.job_available = False
self.printer_available = False
self.available = False
self.printer_error_logged = False
self.job_error_logged = False
self.bed = bed
self.number_of_tools = number_of_tools
def get_tools(self):
"""Get the list of tools that temperature is monitored on."""
tools = []
if self.number_of_tools > 0:
# tools start at 1 bed is 0
for tool_number in range(1, self.number_of_tools+1):
tools.append(tool_number)#'tool' + str(tool_number))
if self.bed:
tools.append('bed')
if not self.bed and self.number_of_tools == 0:
temps = self.printer_last_reading[0].get('temperature')
if temps is not None:
tools = temps.keys()
return tools
def get(self, endpoint):
"""Send a get request, and return the response as a dict."""
# Only query the API at most every 30 seconds
now = time.time()
if endpoint == 'job':
last_time = self.job_last_reading[1]
if last_time is not None:
if now - last_time < 30.0:
return self.job_last_reading[0]
elif endpoint == 'printer':
last_time = self.printer_last_reading[1]
if last_time is not None:
if now - last_time < 30.0:
return self.printer_last_reading[0]
url = self.api_url #+ endpoint
try:
response = requests.get(
url, headers=self.headers, timeout=9)
response.raise_for_status()
if endpoint == 'job':
self.job_last_reading[0] = response.json()
self.job_last_reading[1] = time.time()
self.job_available = True
elif endpoint == 'printer':
self.printer_last_reading[0] = response.json()
self.printer_last_reading[1] = time.time()
self.printer_available = True
self.available = self.printer_available and self.job_available
if self.available:
self.job_error_logged = False
self.printer_error_logged = False
return response.json()
except Exception as conn_exc: # pylint: disable=broad-except
log_string = "Failed to update OctoPrint status. " + \
" Error: %s" % (conn_exc)
# Only log the first failure
if endpoint == 'job':
log_string = "Endpoint: job " + log_string
if not self.job_error_logged:
_LOGGER.error(log_string)
self.job_error_logged = True
self.job_available = False
elif endpoint == 'printer':
log_string = "Endpoint: printer " + log_string
if not self.printer_error_logged:
_LOGGER.error(log_string)
self.printer_error_logged = True
self.printer_available = False
self.available = False
return None
def update(self, sensor_type, end_point, group, tool=None):
"""Return the value for sensor_type from the provided endpoint."""
response = self.get(end_point)
if response is not None:
return get_value_from_json(response, end_point, sensor_type, group, tool)
return response
def get_value_from_json(json_dict, end_point, sensor_type, group, tool):
"""Return the value for sensor_type from the JSON."""
if end_point == 'temps':
if sensor_type == 'current':
if tool == 'bed':
return json_dict[end_point][sensor_type][0]
else:
return json_dict[end_point][sensor_type][tool]
elif sensor_type == 'active':
if tool == 'bed':
return json_dict[end_point]['bed'][sensor_type]
else:
return json_dict[end_point]['tools'][sensor_type][tool-1][tool-1]
else:
if group not in json_dict:
return 0
if group == 'timesLeft':
return json_dict[group]['file']
#if sensor_type in json_dict[group]:
return json_dict[group]
return None