window-time 0.1.0__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.
@@ -0,0 +1,32 @@
1
+ Metadata-Version: 2.4
2
+ Name: window-time
3
+ Version: 0.1.0
4
+ Summary: A daemon that monitors indoor/outdoor temperatures and notifies when to open or close windows
5
+ Project-URL: Homepage, https://github.com/gene1wood/window-time
6
+ Project-URL: Repository, https://github.com/gene1wood/window-time
7
+ Project-URL: Issues, https://github.com/gene1wood/window-time/issues
8
+ Author: Gene Wood
9
+ License-Expression: MIT
10
+ Keywords: home-automation,influxdb,notifications,temperature
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Environment :: Console
13
+ Classifier: Intended Audience :: End Users/Desktop
14
+ Classifier: License :: OSI Approved :: MIT License
15
+ Classifier: Operating System :: OS Independent
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.9
18
+ Classifier: Programming Language :: Python :: 3.10
19
+ Classifier: Programming Language :: Python :: 3.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Topic :: Home Automation
22
+ Requires-Python: >=3.10
23
+ Requires-Dist: influxdb-client>=1.36.0
24
+ Requires-Dist: numpy>=1.24.0
25
+ Requires-Dist: platformdirs>=4.0.0
26
+ Requires-Dist: pyyaml>=6.0
27
+ Requires-Dist: requests>=2.28.0
28
+ Provides-Extra: dev
29
+ Requires-Dist: black>=23.0.0; extra == 'dev'
30
+ Requires-Dist: pytest-cov>=4.0.0; extra == 'dev'
31
+ Requires-Dist: pytest>=7.0.0; extra == 'dev'
32
+ Requires-Dist: ruff>=0.1.0; extra == 'dev'
@@ -0,0 +1,68 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "window-time"
7
+ version = "0.1.0"
8
+ description = "A daemon that monitors indoor/outdoor temperatures and notifies when to open or close windows"
9
+ license = "MIT"
10
+ requires-python = ">=3.10"
11
+ authors = [
12
+ { name = "Gene Wood" }
13
+ ]
14
+ keywords = ["temperature", "home-automation", "influxdb", "notifications"]
15
+ classifiers = [
16
+ "Development Status :: 4 - Beta",
17
+ "Environment :: Console",
18
+ "Intended Audience :: End Users/Desktop",
19
+ "License :: OSI Approved :: MIT License",
20
+ "Operating System :: OS Independent",
21
+ "Programming Language :: Python :: 3",
22
+ "Programming Language :: Python :: 3.9",
23
+ "Programming Language :: Python :: 3.10",
24
+ "Programming Language :: Python :: 3.11",
25
+ "Programming Language :: Python :: 3.12",
26
+ "Topic :: Home Automation",
27
+ ]
28
+ dependencies = [
29
+ "influxdb-client>=1.36.0",
30
+ "platformdirs>=4.0.0",
31
+ "pyyaml>=6.0",
32
+ "requests>=2.28.0",
33
+ "numpy>=1.24.0",
34
+ ]
35
+
36
+ [project.optional-dependencies]
37
+ dev = [
38
+ "pytest>=7.0.0",
39
+ "pytest-cov>=4.0.0",
40
+ "black>=23.0.0",
41
+ "ruff>=0.1.0",
42
+ ]
43
+
44
+ [project.scripts]
45
+ window-time = "window_time.cli:main"
46
+
47
+ [project.urls]
48
+ Homepage = "https://github.com/gene1wood/window-time"
49
+ Repository = "https://github.com/gene1wood/window-time"
50
+ Issues = "https://github.com/gene1wood/window-time/issues"
51
+
52
+ [tool.hatch.build.targets.sdist]
53
+ include = [
54
+ "/src",
55
+ ]
56
+
57
+ [tool.hatch.build.targets.wheel]
58
+ packages = ["src/window_time"]
59
+
60
+ [tool.ruff]
61
+ line-length = 100
62
+ target-version = "py310"
63
+
64
+ [tool.ruff.lint]
65
+ select = ["E", "F", "I", "W"]
66
+
67
+ [tool.pytest.ini_options]
68
+ testpaths = ["tests"]
@@ -0,0 +1,3 @@
1
+ """Window Time - Temperature-based window management notifications."""
2
+
3
+ __version__ = "0.1.0"
@@ -0,0 +1,8 @@
1
+ """Allow running as python -m window_time."""
2
+
3
+ import sys
4
+
5
+ from .cli import main
6
+
7
+ if __name__ == "__main__":
8
+ sys.exit(main())
@@ -0,0 +1,361 @@
1
+ """Command-line interface for window-time."""
2
+
3
+ import argparse
4
+ import logging
5
+ import sys
6
+ from datetime import date
7
+ from pathlib import Path
8
+
9
+ from . import __version__
10
+ from .config import (
11
+ Config,
12
+ get_config_path,
13
+ get_state_path,
14
+ load_config,
15
+ save_default_config,
16
+ )
17
+ from .daemon import WindowTimeDaemon, run_daemon
18
+ from .season import get_season
19
+
20
+
21
+ def setup_logging(verbose: bool = False, debug: bool = False) -> None:
22
+ """Configure logging for the application.
23
+
24
+ Args:
25
+ verbose: Enable verbose output.
26
+ debug: Enable debug output.
27
+ """
28
+ if debug:
29
+ level = logging.DEBUG
30
+ elif verbose:
31
+ level = logging.INFO
32
+ else:
33
+ level = logging.WARNING
34
+
35
+ logging.basicConfig(
36
+ level=level,
37
+ format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
38
+ datefmt="%Y-%m-%d %H:%M:%S",
39
+ )
40
+
41
+
42
+ def cmd_run(args: argparse.Namespace, config: Config) -> int:
43
+ """Run the daemon.
44
+
45
+ Args:
46
+ args: Parsed command-line arguments.
47
+ config: Application configuration.
48
+
49
+ Returns:
50
+ Exit code.
51
+ """
52
+ run_daemon(config)
53
+ return 0
54
+
55
+
56
+ def cmd_once(args: argparse.Namespace, config: Config) -> int:
57
+ """Run a single temperature check.
58
+
59
+ Args:
60
+ args: Parsed command-line arguments.
61
+ config: Application configuration.
62
+
63
+ Returns:
64
+ Exit code.
65
+ """
66
+ daemon = WindowTimeDaemon(config)
67
+ daemon.run_once()
68
+ return 0
69
+
70
+
71
+ def cmd_status(args: argparse.Namespace, config: Config) -> int:
72
+ """Show current status and temperatures.
73
+
74
+ Args:
75
+ args: Parsed command-line arguments.
76
+ config: Application configuration.
77
+
78
+ Returns:
79
+ Exit code.
80
+ """
81
+ from .influxdb_client import TemperatureClient
82
+ from .temperature_analysis import analyze_temperatures
83
+ from .window_state import load_state
84
+
85
+ print("Window Time Status")
86
+ print("==================")
87
+ print()
88
+
89
+ # Show season
90
+ today = date.today()
91
+ season = get_season(today, config.season)
92
+ print(f"Date: {today.isoformat()}")
93
+ print(f"Season: {season.value.capitalize()}")
94
+ print()
95
+
96
+ # Show current state
97
+ state = load_state()
98
+ print("Window State")
99
+ print("------------")
100
+ print(f"Last known state: {state.last_known_state.value}")
101
+ print(f"Windows opened for warmth today: {state.windows_opened_for_warmth}")
102
+ if state.last_outdoor_warmer is not None:
103
+ outdoor_status = "warmer" if state.last_outdoor_warmer else "cooler"
104
+ print(f"Last check: Outdoor was {outdoor_status} than indoor")
105
+ print()
106
+
107
+ # Fetch and show temperatures
108
+ print("Current Temperatures")
109
+ print("--------------------")
110
+ try:
111
+ with TemperatureClient(config) as client:
112
+ temp_data = client.fetch_temperature_data(lookback_hours=config.daemon.lookback_hours)
113
+
114
+ if not temp_data.has_data:
115
+ print("No temperature data available")
116
+ return 1
117
+
118
+ analysis = analyze_temperatures(temp_data)
119
+
120
+ if analysis.current_indoor_temp is not None:
121
+ print(f"Indoor: {analysis.current_indoor_temp:.1f}°")
122
+ if analysis.current_outdoor_temp is not None:
123
+ print(f"Outdoor: {analysis.current_outdoor_temp:.1f}°")
124
+
125
+ if analysis.outdoor_warmer:
126
+ print("Outdoor is currently WARMER than indoor")
127
+ else:
128
+ print("Outdoor is currently COOLER than indoor")
129
+
130
+ print()
131
+ print("Temperature Trends")
132
+ print("------------------")
133
+ if analysis.indoor_trend:
134
+ print(
135
+ f"Indoor: {analysis.indoor_trend.direction.value} "
136
+ f"({analysis.indoor_trend.rate_per_hour:+.2f}°/hr)"
137
+ )
138
+ if analysis.outdoor_trend:
139
+ print(
140
+ f"Outdoor: {analysis.outdoor_trend.direction.value} "
141
+ f"({analysis.outdoor_trend.rate_per_hour:+.2f}°/hr)"
142
+ )
143
+
144
+ if analysis.crossing_prediction and analysis.crossing_prediction.will_cross:
145
+ print()
146
+ print("Crossing Prediction")
147
+ print("-------------------")
148
+ print(
149
+ f"Temperatures will cross in "
150
+ f"{analysis.crossing_prediction.minutes_until_crossing:.0f} minutes"
151
+ )
152
+ print(f"Crossing temperature: {analysis.crossing_prediction.crossing_temp:.1f}°")
153
+ if analysis.crossing_prediction.crossing_time:
154
+ print(
155
+ f"Crossing time: {analysis.crossing_prediction.crossing_time.strftime('%H:%M')}"
156
+ )
157
+
158
+ except Exception as e:
159
+ print(f"Error fetching temperature data: {e}")
160
+ return 1
161
+
162
+ return 0
163
+
164
+
165
+ def cmd_init(args: argparse.Namespace, config: Config) -> int:
166
+ """Initialize configuration file.
167
+
168
+ Args:
169
+ args: Parsed command-line arguments.
170
+ config: Application configuration.
171
+
172
+ Returns:
173
+ Exit code.
174
+ """
175
+ config_path = Path(args.config) if args.config else None
176
+
177
+ if config_path and config_path.exists():
178
+ print(f"Configuration file already exists at {config_path}")
179
+ if not args.force:
180
+ print("Use --force to overwrite")
181
+ return 1
182
+
183
+ saved_path = save_default_config(config_path)
184
+ print(f"Configuration file created at {saved_path}")
185
+ print()
186
+ print("Please edit the configuration file to add your:")
187
+ print(" - InfluxDB token")
188
+ print(" - ntfy.sh topic ID")
189
+ print(" - Any other custom settings")
190
+ return 0
191
+
192
+
193
+ def cmd_config(args: argparse.Namespace, config: Config) -> int:
194
+ """Show configuration paths and current settings.
195
+
196
+ Args:
197
+ args: Parsed command-line arguments.
198
+ config: Application configuration.
199
+
200
+ Returns:
201
+ Exit code.
202
+ """
203
+ print("Configuration Paths")
204
+ print("===================")
205
+ print(f"Config file: {get_config_path()}")
206
+ print(f"State file: {get_state_path()}")
207
+ print()
208
+
209
+ if args.show_values:
210
+ print("Current Configuration")
211
+ print("=====================")
212
+ print()
213
+ print("InfluxDB:")
214
+ print(f" URL: {config.influxdb.url}")
215
+ print(f" Organization: {config.influxdb.org}")
216
+ print(f" Bucket: {config.influxdb.bucket}")
217
+ print(f" Measurement: {config.influxdb.measurement}")
218
+ print(f" Token: {'[set]' if config.influxdb.token else '[not set]'}")
219
+ print()
220
+ print("Notification:")
221
+ print(f" Server: {config.notification.ntfy_server}")
222
+ print(f" Topic: {config.notification.ntfy_topic or '[not set]'}")
223
+ print()
224
+ print("Season Boundaries:")
225
+ print(
226
+ f" Summer: {config.season.summer_start_month}/{config.season.summer_start_day} "
227
+ f"to {config.season.summer_end_month}/{config.season.summer_end_day}"
228
+ )
229
+ print()
230
+ print("Daemon:")
231
+ print(f" Interval: {config.daemon.interval_minutes} minutes")
232
+ print(f" Lookback: {config.daemon.lookback_hours} hour(s)")
233
+
234
+ return 0
235
+
236
+
237
+ def create_parser() -> argparse.ArgumentParser:
238
+ """Create the argument parser.
239
+
240
+ Returns:
241
+ Configured ArgumentParser.
242
+ """
243
+ parser = argparse.ArgumentParser(
244
+ prog="window-time",
245
+ description="Monitor indoor/outdoor temperatures and notify when to open or close windows",
246
+ )
247
+
248
+ parser.add_argument(
249
+ "-V",
250
+ "--version",
251
+ action="version",
252
+ version=f"%(prog)s {__version__}",
253
+ )
254
+
255
+ parser.add_argument(
256
+ "-c",
257
+ "--config",
258
+ metavar="PATH",
259
+ help="Path to configuration file (default: XDG config location)",
260
+ )
261
+
262
+ parser.add_argument(
263
+ "-v",
264
+ "--verbose",
265
+ action="store_true",
266
+ help="Enable verbose output",
267
+ )
268
+
269
+ parser.add_argument(
270
+ "--debug",
271
+ action="store_true",
272
+ help="Enable debug output",
273
+ )
274
+
275
+ subparsers = parser.add_subparsers(
276
+ title="commands",
277
+ dest="command",
278
+ metavar="COMMAND",
279
+ )
280
+
281
+ # run command (default)
282
+ run_parser = subparsers.add_parser(
283
+ "run",
284
+ help="Run the daemon (default command)",
285
+ )
286
+ run_parser.set_defaults(func=cmd_run)
287
+
288
+ # once command
289
+ once_parser = subparsers.add_parser(
290
+ "once",
291
+ help="Run a single temperature check and exit",
292
+ )
293
+ once_parser.set_defaults(func=cmd_once)
294
+
295
+ # status command
296
+ status_parser = subparsers.add_parser(
297
+ "status",
298
+ help="Show current status and temperatures",
299
+ )
300
+ status_parser.set_defaults(func=cmd_status)
301
+
302
+ # init command
303
+ init_parser = subparsers.add_parser(
304
+ "init",
305
+ help="Initialize configuration file",
306
+ )
307
+ init_parser.add_argument(
308
+ "-f",
309
+ "--force",
310
+ action="store_true",
311
+ help="Overwrite existing configuration file",
312
+ )
313
+ init_parser.set_defaults(func=cmd_init)
314
+
315
+ # config command
316
+ config_parser = subparsers.add_parser(
317
+ "config",
318
+ help="Show configuration paths and settings",
319
+ )
320
+ config_parser.add_argument(
321
+ "-s",
322
+ "--show-values",
323
+ action="store_true",
324
+ help="Show current configuration values",
325
+ )
326
+ config_parser.set_defaults(func=cmd_config)
327
+
328
+ return parser
329
+
330
+
331
+ def main(argv: list[str] | None = None) -> int:
332
+ """Main entry point.
333
+
334
+ Args:
335
+ argv: Command-line arguments. If None, uses sys.argv.
336
+
337
+ Returns:
338
+ Exit code.
339
+ """
340
+ parser = create_parser()
341
+ args = parser.parse_args(argv)
342
+
343
+ setup_logging(verbose=args.verbose, debug=args.debug)
344
+
345
+ # Load configuration
346
+ config_path = Path(args.config) if args.config else None
347
+ try:
348
+ config = load_config(config_path)
349
+ except Exception as e:
350
+ print(f"Error loading configuration: {e}", file=sys.stderr)
351
+ return 1
352
+
353
+ # Default to 'run' command if no command specified
354
+ if args.command is None:
355
+ args.func = cmd_run
356
+
357
+ return args.func(args, config)
358
+
359
+
360
+ if __name__ == "__main__":
361
+ sys.exit(main())