commit
3da06fe888
@ -0,0 +1,123 @@
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
pip-wheel-metadata/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
|
||||
# PyBuilder
|
||||
target/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
|
||||
# IPython
|
||||
profile_default/
|
||||
ipython_config.py
|
||||
|
||||
# pyenv
|
||||
.python-version
|
||||
|
||||
# pipenv
|
||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||
# install all needed dependencies.
|
||||
#Pipfile.lock
|
||||
|
||||
# celery beat schedule file
|
||||
celerybeat-schedule
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2018 Thomas Wilson
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@ -0,0 +1,8 @@
|
||||
# Shepherd
|
||||
|
||||
This is a project to try and standardise the management of physically remote
|
||||
nodes (used mainly for solar powered sensors and actuators on farms).
|
||||
|
||||
Shepherd provides a modular interface to define configuration parameters and
|
||||
schedule behaviour, allowing remote config changes while reducing the risk of breaking
|
||||
things (requiring a long trip out to the node to recover it).
|
||||
@ -0,0 +1,20 @@
|
||||
from setuptools import setup
|
||||
|
||||
setup(
|
||||
name='shepherd',
|
||||
version='0.2dev',
|
||||
author='Thomas Wilson',
|
||||
author_email='t.wilson@distreon.com.au',
|
||||
packages=['shepherd', ],
|
||||
install_requires=[
|
||||
'toml',
|
||||
'apscheduler',
|
||||
'paramiko'
|
||||
],
|
||||
entry_points={
|
||||
'console_scripts': ['shepherd=shepherd.core:main'],
|
||||
},
|
||||
license='MIT license',
|
||||
description='Herd your mob of physically remote nodes',
|
||||
long_description=open('README.md').read(),
|
||||
)
|
||||
@ -0,0 +1,13 @@
|
||||
# Should be copied to /lib/systemd/system/ then enabled with
|
||||
# sudo systemctl enable shepherd.service
|
||||
[Unit]
|
||||
Description=Monitoring and control service for remote nodes
|
||||
After=multi-user.target
|
||||
|
||||
[Service]
|
||||
Type=simple
|
||||
Environment=PYTHONUNBUFFERED=true
|
||||
ExecStart=/usr/local/bin/shepherd /home/pi/shepherd.toml
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
@ -0,0 +1,223 @@
|
||||
import re
|
||||
import toml
|
||||
|
||||
|
||||
class InvalidConfigError(Exception):
|
||||
pass
|
||||
|
||||
# On start,
|
||||
# get conf_defs via imports in core
|
||||
# Load a conf file
|
||||
# Validate the Shepherd table in conf file, then load its values to get module list
|
||||
# Validate other loaded tables/modules, then load their values
|
||||
|
||||
# How do modules wind up with their instance of ConfigValues? Return val instance
|
||||
# from validation function - as it needs to build the vals instance while validating anyway
|
||||
|
||||
# Validate a conf file given a module or config_def list
|
||||
|
||||
|
||||
# idea is to create these similar to how arg parser works
|
||||
|
||||
# how to store individual items in def? Dict of...
|
||||
# Tables need a dict, lists/arrays need a list of dicts, but what's in the dict?
|
||||
# if it's another instacnce of ConfigDef, then each Config Def needs to handle
|
||||
# one item, but cater for all the different types of items - and many of those
|
||||
# should be able to add a new item.
|
||||
|
||||
# could have a separate class for the root, but lower tables really need to
|
||||
# perform exactly the same...
|
||||
|
||||
# config def required interface:
|
||||
# Validate values.
|
||||
|
||||
class _ConfigDefinition():
|
||||
def __init__(self, default=None, optional=False):
|
||||
self.default = default
|
||||
self.optional = optional
|
||||
|
||||
def validate(self, value): # pylint: disable=W0613
|
||||
raise TypeError("_ConfigDefinition.validate() is an abstract method")
|
||||
|
||||
|
||||
class BoolDef(_ConfigDefinition):
|
||||
def __init__(self, default=None, optional=False): # pylint: disable=W0235
|
||||
super().__init__(default, optional)
|
||||
|
||||
def validate(self, value):
|
||||
if not isinstance(value, bool):
|
||||
raise InvalidConfigError("Config value must be a boolean")
|
||||
|
||||
|
||||
class IntDef(_ConfigDefinition):
|
||||
def __init__(self, default=None, minval=None, maxval=None,
|
||||
optional=False):
|
||||
super().__init__(default, optional)
|
||||
self.minval = minval
|
||||
self.maxval = maxval
|
||||
|
||||
def validate(self, value):
|
||||
if not isinstance(value, int):
|
||||
raise InvalidConfigError("Config value must be an integer")
|
||||
if self.minval is not None and value < self.minval:
|
||||
raise InvalidConfigError("Config value must be >= " +
|
||||
str(self.minval))
|
||||
if self.maxval is not None and value > self.maxval:
|
||||
raise InvalidConfigError("Config value must be <= " +
|
||||
str(self.maxval))
|
||||
|
||||
|
||||
class StringDef(_ConfigDefinition):
|
||||
def __init__(self, default=None, minlength=None, maxlength=None,
|
||||
optional=False):
|
||||
super().__init__(default, optional)
|
||||
self.minlength = minlength
|
||||
self.maxlength = maxlength
|
||||
|
||||
def validate(self, value):
|
||||
if not isinstance(value, str):
|
||||
raise InvalidConfigError("Config value must be a string")
|
||||
if self.minlength is not None and len(value) < self.minlength:
|
||||
raise InvalidConfigError("Config string length must be >= " +
|
||||
str(self.minlength))
|
||||
if self.maxlength is not None and len(value) > self.maxlength:
|
||||
raise InvalidConfigError("Config string length must be <= " +
|
||||
str(self.maxlength))
|
||||
|
||||
|
||||
class TableDef(_ConfigDefinition):
|
||||
def __init__(self, default=None, optional=False):
|
||||
super().__init__(default, optional)
|
||||
self.def_table = {}
|
||||
|
||||
def add_def(self, name, newdef):
|
||||
if not isinstance(newdef, _ConfigDefinition):
|
||||
raise TypeError("Config definiton must be an instance of a "
|
||||
"ConfigDefinition subclass")
|
||||
if not isinstance(name, str):
|
||||
raise TypeError("Config definition name must be a string")
|
||||
self.def_table[name] = newdef
|
||||
return newdef
|
||||
|
||||
def validate(self, value_table): # pylint: disable=W0221
|
||||
def_set = set(self.def_table.keys())
|
||||
value_set = set(value_table.keys())
|
||||
|
||||
for missing_key in def_set - value_set:
|
||||
if not self.def_table[missing_key].optional:
|
||||
raise InvalidConfigError("Table must contain key: " +
|
||||
missing_key)
|
||||
else:
|
||||
value_table[missing_key] = self.def_table[missing_key].default
|
||||
|
||||
for extra_key in value_set - def_set:
|
||||
raise InvalidConfigError("Table contains unknown key: " +
|
||||
extra_key)
|
||||
|
||||
for key, value in value_table.items():
|
||||
try:
|
||||
self.def_table[key].validate(value)
|
||||
except InvalidConfigError as e:
|
||||
e.args = ("Key: " + key,) + e.args
|
||||
raise
|
||||
|
||||
|
||||
class _ArrayDefMixin():
|
||||
def validate(self, value_array):
|
||||
if not isinstance(value_array, list):
|
||||
raise InvalidConfigError("Config item must be an array")
|
||||
for index, value in enumerate(value_array):
|
||||
try:
|
||||
super().validate(value)
|
||||
except InvalidConfigError as e:
|
||||
e.args = ("Array index: " + str(index),) + e.args
|
||||
raise
|
||||
|
||||
|
||||
class BoolArrayDef(_ArrayDefMixin, BoolDef):
|
||||
pass
|
||||
|
||||
|
||||
class IntArrayDef(_ArrayDefMixin, IntDef):
|
||||
pass
|
||||
|
||||
|
||||
class StringArrayDef(_ArrayDefMixin, StringDef):
|
||||
pass
|
||||
|
||||
|
||||
class TableArrayDef(_ArrayDefMixin, TableDef):
|
||||
pass
|
||||
|
||||
|
||||
class ConfDefinition(TableDef):
|
||||
pass
|
||||
|
||||
|
||||
class ConfigManager():
|
||||
def __init__(self):
|
||||
self.root_config = {}
|
||||
|
||||
def load(self, source):
|
||||
if isinstance(source, dict): # load from dict
|
||||
self.root_config = source
|
||||
elif isinstance(source, str): # load from pathname
|
||||
with open(source, 'r') as conf_file:
|
||||
self.root_config = toml.load(conf_file)
|
||||
else: # load from file
|
||||
self.root_config = toml.load(source)
|
||||
|
||||
def get_config(self, table_name, conf_def):
|
||||
if not isinstance(conf_def, ConfDefinition):
|
||||
raise TypeError("Supplied config definition must be an instance "
|
||||
"of ConfDefinition")
|
||||
if table_name not in self.root_config:
|
||||
raise InvalidConfigError("Config must contain table: " + table_name)
|
||||
try:
|
||||
conf_def.validate(self.root_config[table_name])
|
||||
except InvalidConfigError as e:
|
||||
e.args = ("Module: " + table_name,) + e.args
|
||||
raise
|
||||
return self.root_config[table_name]
|
||||
|
||||
def get_configs(self, conf_defs):
|
||||
config_values = {}
|
||||
for name, conf_def in conf_defs.items():
|
||||
config_values[name] = self.get_config(name, conf_def)
|
||||
return config_values
|
||||
|
||||
def get_module_configs(self, modules):
|
||||
config_values = {}
|
||||
for name, module in modules.items():
|
||||
config_values[name] = self.get_config(name, module.conf_def)
|
||||
return config_values
|
||||
|
||||
def dump_toml(self):
|
||||
return toml.dumps(self.root_config)
|
||||
|
||||
def dump_to_file(self, filepath, message=None):
|
||||
with open(filepath, 'w+') as f:
|
||||
content = self.dump_toml()
|
||||
if message is not None:
|
||||
content = content.rstrip() + gen_comment(message)
|
||||
f.write(content)
|
||||
|
||||
|
||||
def strip_toml_message(string):
|
||||
print("stripping...")
|
||||
return re.sub("(?m)^#\\ shepherd_message:[^\\n]*$\\n?(?:^#[^\\n]+$\\n?)*",
|
||||
'', string)
|
||||
|
||||
|
||||
def update_toml_message(filepath, message):
|
||||
with open(filepath, 'r+') as f:
|
||||
content = f.read()
|
||||
content = strip_toml_message(content).rstrip()
|
||||
content += gen_comment(message)
|
||||
f.seek(0)
|
||||
f.write(content)
|
||||
f.truncate()
|
||||
|
||||
|
||||
def gen_comment(string):
|
||||
return '\n# shepherd_message: ' + '\n# '.join(string.replace('#', '').splitlines()) + '\n'
|
||||
@ -0,0 +1,138 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
# depends on:
|
||||
# python 3.4 (included in Raspbian Jessie)
|
||||
# APScheduler
|
||||
|
||||
|
||||
import sys
|
||||
import argparse
|
||||
import os
|
||||
import toml
|
||||
|
||||
|
||||
import shepherd.config
|
||||
import shepherd.module
|
||||
|
||||
from apscheduler.schedulers.blocking import BlockingScheduler
|
||||
from datetime import datetime
|
||||
|
||||
from types import SimpleNamespace
|
||||
|
||||
|
||||
# Future implementations of checking config differences should be done on
|
||||
# the hash of the nested conf dict, so comments shouldn't affect this.
|
||||
|
||||
# save old config to somewhere in the shepherd root dir - probably need to
|
||||
# implement a TOML writer in the config module.
|
||||
|
||||
# later on, there's going to be an issue with a new config being applied
|
||||
# remotely, then the system restarting, and an old edit in /boot being
|
||||
# applied over the top...
|
||||
# Fix this by saving the working config to /boot when new config applied
|
||||
# remotely.
|
||||
|
||||
def load_config(config_path):
|
||||
confman = shepherd.config.ConfigManager()
|
||||
confman.load(os.path.expanduser(config_path))
|
||||
core_conf = confman.get_config("shepherd", core_confdef())
|
||||
|
||||
# Check for an edit_conf file, and try to load it
|
||||
try:
|
||||
edit_confman = shepherd.config.ConfigManager()
|
||||
edit_confman.load(os.path.expanduser(core_conf["conf_edit_path"]))
|
||||
core_edit_conf = edit_confman.get_config("shepherd", core_confdef())
|
||||
|
||||
mod_classes = shepherd.module.find_modules(core_edit_conf["modules"])
|
||||
mod_configs = edit_confman.get_module_configs(mod_classes)
|
||||
|
||||
except FileNotFoundError:
|
||||
conf_edit_message = None
|
||||
except shepherd.config.InvalidConfigError as e:
|
||||
conf_edit_message = "Invalid config.\n " + str(e.args)
|
||||
except toml.TomlDecodeError as e:
|
||||
conf_edit_message = "TOML syntax error.\n" + str(e)
|
||||
except Exception:
|
||||
conf_edit_message = "Error processing new config"
|
||||
else:
|
||||
conf_edit_message = ("Successfully applied this config at:" +
|
||||
str(datetime.now()))
|
||||
confman = edit_confman
|
||||
core_conf = core_edit_conf
|
||||
|
||||
if conf_edit_message is not None:
|
||||
shepherd.config.update_toml_message(
|
||||
os.path.expanduser(core_conf["conf_edit_path"]), conf_edit_message)
|
||||
|
||||
# if editconf failed, load current config
|
||||
if confman is not edit_confman:
|
||||
mod_classes = shepherd.module.find_modules(core_conf["modules"])
|
||||
mod_configs = confman.get_module_configs(mod_classes)
|
||||
|
||||
# If no editconf file was found, write out the current config as a template
|
||||
if conf_edit_message is None:
|
||||
confman.dump_to_file(os.path.expanduser(core_conf["conf_edit_path"]),
|
||||
"Config generated at:" + str(datetime.now()))
|
||||
|
||||
return (core_conf, mod_classes, mod_configs)
|
||||
|
||||
|
||||
def core_confdef():
|
||||
confdef = shepherd.config.ConfDefinition()
|
||||
confdef.add_def("id", shepherd.config.StringDef())
|
||||
confdef.add_def("modules", shepherd.config.StringArrayDef())
|
||||
confdef.add_def("root_dir", shepherd.config.StringDef())
|
||||
confdef.add_def("conf_edit_path", shepherd.config.StringDef())
|
||||
return confdef
|
||||
|
||||
|
||||
class ShepherdInterface(shepherd.module.Interface):
|
||||
def __init__(self, scheduler, config):
|
||||
super().__init__(None)
|
||||
self.id = config["id"]
|
||||
self.root_dir = config["root_dir"]
|
||||
self.scheduler = scheduler
|
||||
|
||||
|
||||
def main():
|
||||
argparser = argparse.ArgumentParser(description="Keep track of a mob "
|
||||
"of roaming Pis")
|
||||
argparser.add_argument("configfile", nargs='?', metavar="configfile",
|
||||
help="Path to configfile", default="shepherd.toml")
|
||||
|
||||
args = argparser.parse_args()
|
||||
|
||||
confman = shepherd.config.ConfigManager()
|
||||
confman.load(os.path.expanduser(args.configfile))
|
||||
core_conf = confman.get_config("shepherd", core_confdef())
|
||||
breakpoint()
|
||||
|
||||
(core_conf, mod_classes, mod_configs) = load_config(args.configfile)
|
||||
|
||||
scheduler = BlockingScheduler()
|
||||
core_interface = ShepherdInterface(scheduler, core_conf)
|
||||
|
||||
# get validated config values for modules, then instantiate them
|
||||
modules = {}
|
||||
mod_interfaces = {}
|
||||
for name, mod_class in mod_classes.items():
|
||||
modules[name] = mod_class(mod_configs[name], core_interface)
|
||||
mod_interfaces[name] = modules[name].interface
|
||||
|
||||
# run post init after all modules are loaded to allow them to hook in to
|
||||
# each other
|
||||
mod_interfaces = SimpleNamespace(**mod_interfaces)
|
||||
for module in modules.values():
|
||||
module.init_other_interfaces(mod_interfaces)
|
||||
|
||||
print(str(datetime.now()))
|
||||
print('Press Ctrl+{0} to exit'.format('Break' if os.name == 'nt' else 'C'))
|
||||
|
||||
try:
|
||||
scheduler.start()
|
||||
except (KeyboardInterrupt, SystemExit):
|
||||
pass
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@ -0,0 +1,84 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
from contextlib import suppress
|
||||
import importlib
|
||||
|
||||
|
||||
class Hook():
|
||||
def __init__(self):
|
||||
self.attached_functions = []
|
||||
|
||||
def attach(self, new_func):
|
||||
if not callable(new_func):
|
||||
raise TypeError("Argument to Hook.attach must be callable")
|
||||
self.attached_functions.append(new_func)
|
||||
|
||||
def __call__(self, *args, **kwargs):
|
||||
for func in self.attached_functions:
|
||||
func(*args, **kwargs)
|
||||
|
||||
|
||||
class Module():
|
||||
def __init__(self, config, core_interface):
|
||||
self.config = config
|
||||
self.shepherd = core_interface
|
||||
#self.shepherd.scheduler
|
||||
|
||||
self.interface = Interface(self)
|
||||
self.modules = {}
|
||||
# dummy interface in case module doesn't want one
|
||||
|
||||
def init_other_interfaces(self, interfaces):
|
||||
if not isinstance(self.interface, Interface):
|
||||
raise TypeError("shepherd.module.Module interface attribute must "
|
||||
"be subclass of type shepherd.module.Interface")
|
||||
self.modules = interfaces
|
||||
|
||||
|
||||
# Look at providing a run() function or similar, which is a thread started
|
||||
# post_modules_setup
|
||||
class SimpleModule(Module):
|
||||
def __init__(self, config, core_interface):
|
||||
super().__init__(config, core_interface)
|
||||
self.setup()
|
||||
|
||||
def init_other_interfaces(self, interfaces):
|
||||
super().init_other_interfaces(interfaces)
|
||||
self.setup_other_modules()
|
||||
|
||||
# Users override this, self.shepherd and self.config are available now.
|
||||
# User still needs to define self.interface if it is used.
|
||||
def setup(self):
|
||||
pass
|
||||
|
||||
def setup_other_modules(self):
|
||||
pass
|
||||
|
||||
|
||||
"""
|
||||
An interface to a Shepherd module, accessible by other modules.
|
||||
All public methods in a module interface need to be threadsafe, as they will
|
||||
be called by other modules (which generally run in a seperate thread)
|
||||
"""
|
||||
class Interface():
|
||||
def __init__(self, module):
|
||||
self._module = module
|
||||
|
||||
|
||||
def find_modules(module_names):
|
||||
module_classes = {}
|
||||
for module_name in module_names:
|
||||
mod = importlib.import_module("shepherd.modules." + module_name)
|
||||
attrs = [getattr(mod, name) for name in dir(mod)]
|
||||
|
||||
for attr in attrs:
|
||||
with suppress(TypeError):
|
||||
if issubclass(attr, Module):
|
||||
module_classes[module_name] = attr
|
||||
break
|
||||
else:
|
||||
raise Exception("Imported shepherd modules must contain a "
|
||||
"subclass of shepherd.module.Module, such as"
|
||||
"shepherd.module.SimpleModule")
|
||||
|
||||
return module_classes
|
||||
@ -0,0 +1,77 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import shepherd.config
|
||||
import shepherd.module
|
||||
|
||||
import sys
|
||||
import os
|
||||
import time
|
||||
import argparse
|
||||
|
||||
from gpiozero import OutputDevice, Device
|
||||
from gpiozero.pins.pigpio import PiGPIOFactory
|
||||
|
||||
from shepherd.modules.betterservo import BetterServo
|
||||
|
||||
Device.pin_factory = PiGPIOFactory()
|
||||
|
||||
|
||||
APHIDTRAP_LED_PIN = 5 #Out2
|
||||
|
||||
|
||||
class AphidtrapConfDef(shepherd.config.ConfDefinition):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
|
||||
|
||||
class AphidtrapModule(shepherd.module.SimpleModule):
|
||||
conf_def = AphidtrapConfDef()
|
||||
|
||||
def setup(self):
|
||||
|
||||
print("Aphidtrap config:")
|
||||
print(self.config)
|
||||
|
||||
self.led_power = OutputDevice(APHIDTRAP_LED_PIN,
|
||||
active_high=True,
|
||||
initial_value=False)
|
||||
|
||||
def setup_other_modules(self):
|
||||
self.modules.picam.hook_pre_cam.attach(self.led_on)
|
||||
self.modules.picam.hook_post_cam.attach(self.led_off)
|
||||
|
||||
def led_on(self):
|
||||
self.led_power.on()
|
||||
|
||||
def led_off(self):
|
||||
self.led_power.off()
|
||||
|
||||
|
||||
|
||||
def main(argv):
|
||||
argparser = argparse.ArgumentParser(
|
||||
description='Module for aphidtrap control functions. Run for testing')
|
||||
argparser.add_argument("configfile", nargs='?', metavar="configfile",
|
||||
help="Path to configfile", default="conf.toml")
|
||||
|
||||
|
||||
args = argparser.parse_args()
|
||||
confman = shepherd.config.ConfigManager()
|
||||
|
||||
srcdict = {"aphidtrap": {}}
|
||||
|
||||
if os.path.isfile(args.configfile):
|
||||
confman.load(args.configfile)
|
||||
else:
|
||||
confman.load(srcdict)
|
||||
|
||||
aphidtrap_mod = AphidtrapModule(confman.get_config("aphid", AphidtrapConfDef()),
|
||||
shepherd.module.Interface(None))
|
||||
|
||||
aphidtrap_mod.led_on()
|
||||
time.sleep(2)
|
||||
aphidtrap_mod.led_off()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main(sys.argv[1:])
|
||||
@ -0,0 +1,147 @@
|
||||
from gpiozero import PWMOutputDevice, SourceMixin, CompositeDevice
|
||||
|
||||
|
||||
class BetterServo(SourceMixin, CompositeDevice):
|
||||
"""
|
||||
Copy of GPIOZero servo, but with control over pulse width and active_high
|
||||
"""
|
||||
def __init__(
|
||||
self, pin=None, initial_value=0.0,
|
||||
min_pulse_width=1/1000, max_pulse_width=2/1000,
|
||||
frame_width=20/1000, pin_factory=None, active_high=True):
|
||||
if min_pulse_width >= max_pulse_width:
|
||||
raise ValueError('min_pulse_width must be less than max_pulse_width')
|
||||
if max_pulse_width >= frame_width:
|
||||
raise ValueError('max_pulse_width must be less than frame_width')
|
||||
self._frame_width = frame_width
|
||||
self._min_dc = min_pulse_width / frame_width
|
||||
self._dc_range = (max_pulse_width - min_pulse_width) / frame_width
|
||||
self._min_value = -1
|
||||
self._value_range = 2
|
||||
super(BetterServo, self).__init__(
|
||||
pwm_device=PWMOutputDevice(
|
||||
pin, frequency=int(1 / frame_width), pin_factory=pin_factory,
|
||||
active_high=False
|
||||
),
|
||||
pin_factory=pin_factory
|
||||
)
|
||||
self.pwm_device.active_high=active_high
|
||||
try:
|
||||
self.value = initial_value
|
||||
except:
|
||||
self.close()
|
||||
raise
|
||||
|
||||
@property
|
||||
def frame_width(self):
|
||||
"""
|
||||
The time between control pulses, measured in seconds.
|
||||
"""
|
||||
return self._frame_width
|
||||
|
||||
@property
|
||||
def min_pulse_width(self):
|
||||
"""
|
||||
The control pulse width corresponding to the servo's minimum position,
|
||||
measured in seconds.
|
||||
"""
|
||||
return self._min_dc * self.frame_width
|
||||
|
||||
@property
|
||||
def max_pulse_width(self):
|
||||
"""
|
||||
The control pulse width corresponding to the servo's maximum position,
|
||||
measured in seconds.
|
||||
"""
|
||||
return (self._dc_range * self.frame_width) + self.min_pulse_width
|
||||
|
||||
@property
|
||||
def pulse_width(self):
|
||||
"""
|
||||
Returns the current pulse width controlling the servo.
|
||||
"""
|
||||
if self.pwm_device.frequency is None:
|
||||
return None
|
||||
else:
|
||||
return self.pwm_device.state * self.frame_width
|
||||
|
||||
@pulse_width.setter
|
||||
def pulse_width(self, value):
|
||||
if value is None:
|
||||
self.pwm_device.frequency = None
|
||||
elif self.min_pulse_width <= value <= self.max_pulse_width:
|
||||
self.pwm_device.frequency = int(1 / self.frame_width)
|
||||
self.pwm_device.value = (value / self.frame_width)
|
||||
else:
|
||||
raise OutputDeviceBadValue("Servo pulse_width must be between min and max supplied during construction, or None")
|
||||
|
||||
def min(self):
|
||||
"""
|
||||
Set the servo to its minimum position.
|
||||
"""
|
||||
self.value = -1
|
||||
|
||||
def mid(self):
|
||||
"""
|
||||
Set the servo to its mid-point position.
|
||||
"""
|
||||
self.value = 0
|
||||
|
||||
def max(self):
|
||||
"""
|
||||
Set the servo to its maximum position.
|
||||
"""
|
||||
self.value = 1
|
||||
|
||||
def detach(self):
|
||||
"""
|
||||
Temporarily disable control of the servo. This is equivalent to
|
||||
setting :attr:`value` to ``None``.
|
||||
"""
|
||||
self.value = None
|
||||
|
||||
def _get_value(self):
|
||||
if self.pwm_device.frequency is None:
|
||||
return None
|
||||
else:
|
||||
return (
|
||||
((self.pwm_device.state - self._min_dc) / self._dc_range) *
|
||||
self._value_range + self._min_value)
|
||||
|
||||
@property
|
||||
def value(self):
|
||||
"""
|
||||
Represents the position of the servo as a value between -1 (the minimum
|
||||
position) and +1 (the maximum position). This can also be the special
|
||||
value ``None`` indicating that the servo is currently "uncontrolled",
|
||||
i.e. that no control signal is being sent. Typically this means the
|
||||
servo's position remains unchanged, but that it can be moved by hand.
|
||||
"""
|
||||
result = self._get_value()
|
||||
if result is None:
|
||||
return result
|
||||
else:
|
||||
# NOTE: This round() only exists to ensure we don't confuse people
|
||||
# by returning 2.220446049250313e-16 as the default initial value
|
||||
# instead of 0. The reason _get_value and _set_value are split
|
||||
# out is for descendents that require the un-rounded values for
|
||||
# accuracy
|
||||
return round(result, 14)
|
||||
|
||||
@value.setter
|
||||
def value(self, value):
|
||||
if value is None:
|
||||
self.pwm_device.frequency = None
|
||||
elif -1 <= value <= 1:
|
||||
self.pwm_device.frequency = int(1 / self.frame_width)
|
||||
self.pwm_device.value = (
|
||||
self._min_dc + self._dc_range *
|
||||
((value - self._min_value) / self._value_range)
|
||||
)
|
||||
else:
|
||||
raise OutputDeviceBadValue(
|
||||
"Servo value must be between -1 and 1, or None")
|
||||
|
||||
@property
|
||||
def is_active(self):
|
||||
return self.value is not None
|
||||
@ -0,0 +1,113 @@
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import shepherd.config
|
||||
import shepherd.module
|
||||
|
||||
import sys
|
||||
import os
|
||||
import time
|
||||
import argparse
|
||||
|
||||
from gpiozero import OutputDevice, Device
|
||||
from gpiozero.pins.pigpio import PiGPIOFactory
|
||||
|
||||
from shepherd.modules.betterservo import BetterServo
|
||||
|
||||
Device.pin_factory = PiGPIOFactory()
|
||||
|
||||
|
||||
MOTHTRAP_LED_PIN = 6
|
||||
MOTHTRAP_SERVO_PIN = 10
|
||||
MOTHTRAP_SERVO_POWER_PIN = 9
|
||||
|
||||
|
||||
class MothtrapConfDef(shepherd.config.ConfDefinition):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.add_def('servo_open_pulse', shepherd.config.IntDef(default=1200))
|
||||
self.add_def('servo_closed_pulse', shepherd.config.IntDef(default=1800))
|
||||
self.add_def('servo_open_time', shepherd.config.IntDef(default=5))
|
||||
|
||||
|
||||
class MothtrapModule(shepherd.module.SimpleModule):
|
||||
conf_def = MothtrapConfDef()
|
||||
|
||||
def setup(self):
|
||||
|
||||
print("Mothtrap config:")
|
||||
print(self.config)
|
||||
|
||||
servo_max = self.config["servo_open_pulse"] / 1000000
|
||||
servo_min = self.config["servo_closed_pulse"] / 1000000
|
||||
|
||||
if servo_min > servo_max:
|
||||
servo_min, servo_max = servo_max, servo_min
|
||||
|
||||
self.door_servo = BetterServo(MOTHTRAP_SERVO_PIN, initial_value=None,
|
||||
active_high=False,
|
||||
min_pulse_width=servo_min,
|
||||
max_pulse_width=servo_max)
|
||||
|
||||
self.door_servo_power = OutputDevice(MOTHTRAP_SERVO_POWER_PIN,
|
||||
active_high=True,
|
||||
initial_value=False)
|
||||
|
||||
self.led_power = OutputDevice(MOTHTRAP_LED_PIN,
|
||||
active_high=True,
|
||||
initial_value=False)
|
||||
|
||||
def setup_other_modules(self):
|
||||
self.modules.picam.hook_pre_cam.attach(self.led_on)
|
||||
self.modules.picam.hook_post_cam.attach(self.led_off)
|
||||
self.modules.picam.hook_post_cam.attach(self.run_servo)
|
||||
|
||||
def led_on(self):
|
||||
self.led_power.on()
|
||||
|
||||
def led_off(self):
|
||||
self.led_power.off()
|
||||
|
||||
def run_servo(self):
|
||||
self.door_servo_power.on()
|
||||
time.sleep(0.5)
|
||||
self.door_servo.pulse_width = self.config["servo_open_pulse"] / 1000000
|
||||
time.sleep(self.config["servo_open_time"])
|
||||
self.door_servo.pulse_width = self.config["servo_closed_pulse"] / 1000000
|
||||
time.sleep(self.config["servo_open_time"])
|
||||
self.door_servo.detach()
|
||||
self.door_servo_power.off()
|
||||
|
||||
|
||||
def main(argv):
|
||||
argparser = argparse.ArgumentParser(
|
||||
description='Module for mothtrap control functions. Run for testing')
|
||||
argparser.add_argument("configfile", nargs='?', metavar="configfile",
|
||||
help="Path to configfile", default="conf.toml")
|
||||
argparser.add_argument("test_function", nargs='?', choices=['servo'],
|
||||
help="test function to perform", default="servo")
|
||||
argparser.add_argument("-o", help="servo open position, in us", type=int, default=1200, dest="servo_open_pulse")
|
||||
argparser.add_argument("-c", help="servo closed position, in us", type=int, default=1800, dest="servo_closed_pulse")
|
||||
argparser.add_argument("-w", help="wait time, in seconds", type=int, default=5, dest="servo_open_time")
|
||||
|
||||
args = argparser.parse_args()
|
||||
confman = shepherd.config.ConfigManager()
|
||||
|
||||
srcdict = {"mothtrap": {"servo_open_pulse": args.servo_open_pulse,
|
||||
"servo_closed_pulse":args.servo_closed_pulse,
|
||||
"servo_open_time":args.servo_open_time}}
|
||||
|
||||
if os.path.isfile(args.configfile):
|
||||
confman.load(args.configfile)
|
||||
else:
|
||||
confman.load(srcdict)
|
||||
|
||||
mothtrap_mod = MothtrapModule(confman.get_config("mothtrap", MothtrapConfDef()),
|
||||
shepherd.module.Interface(None))
|
||||
|
||||
mothtrap_mod.led_on()
|
||||
mothtrap_mod.run_servo()
|
||||
mothtrap_mod.led_off()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main(sys.argv[1:])
|
||||
@ -0,0 +1,134 @@
|
||||
import shepherd.config
|
||||
import shepherd.module
|
||||
|
||||
import io
|
||||
import os
|
||||
from datetime import datetime
|
||||
import time
|
||||
from picamera import PiCamera
|
||||
from PIL import Image, ImageDraw, ImageFont
|
||||
|
||||
|
||||
overlayfont = "/usr/share/fonts/truetype/dejavu/DejaVuSansMono.ttf"
|
||||
|
||||
|
||||
class PiCamConfDef(shepherd.config.ConfDefinition):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.add_def('upload_images', shepherd.config.BoolDef(default=False, optional=True))
|
||||
self.add_def('upload_bucket', shepherd.config.StringDef(default="", optional=True))
|
||||
self.add_def('save_directory', shepherd.config.StringDef(default="", optional=False))
|
||||
self.add_def('append_text', shepherd.config.StringDef(default="", optional=True))
|
||||
self.add_def('append_id', shepherd.config.BoolDef(default=True, optional=True))
|
||||
|
||||
array = self.add_def('trigger', shepherd.config.TableArrayDef())
|
||||
array.add_def('hour', shepherd.config.StringDef())
|
||||
array.add_def('minute', shepherd.config.StringDef())
|
||||
array.add_def('second', shepherd.config.StringDef(default="0", optional=True))
|
||||
|
||||
# on server side, we want to be able to list commands that a module responds to
|
||||
# without actually instantiating the module class. Add command templates into
|
||||
# the conf_def, than attach to them in the interface? Was worried about having
|
||||
# "two sources of truth", but you already need to match the conf_def to the
|
||||
# name where you access the value in the module. Could have add_command, which
|
||||
# you then add standard conf_def subclasses to, to reuse validation and server
|
||||
# form generation logic...
|
||||
|
||||
|
||||
class PiCamInterface(shepherd.module.Interface):
|
||||
def __init__(self, module):
|
||||
super().__init__(module)
|
||||
|
||||
self.hook_pre_cam = shepherd.module.Hook()
|
||||
self.hook_post_cam = shepherd.module.Hook()
|
||||
|
||||
# self.add_command("trigger", self.module.camera_job)
|
||||
|
||||
# other module can then call, in init_interfaces, if self.modules.picam is not None:
|
||||
# self.modules.picam.hooks.attach("pre_cam",self.myfunc)
|
||||
# self.modules.picam.pre_cam.attach(self.my_func)
|
||||
|
||||
# self.modules.picam.trigger()
|
||||
|
||||
|
||||
class PiCamModule(shepherd.module.SimpleModule):
|
||||
conf_def = PiCamConfDef()
|
||||
|
||||
def setup(self):
|
||||
self.interface = PiCamInterface(self)
|
||||
# do some camera init stuff
|
||||
|
||||
print("Camera config:")
|
||||
print(self.config)
|
||||
|
||||
if self.config["save_directory"] is "":
|
||||
self.save_directory = os.path.join(self.shepherd.root_dir,
|
||||
"camera")
|
||||
else:
|
||||
self.save_directory = self.config["save_directory"]
|
||||
|
||||
if not os.path.exists(self.save_directory):
|
||||
os.makedirs(self.save_directory)
|
||||
|
||||
#global cam_led
|
||||
#cam_led = LED(CAMERA_LED_PIN, active_high=False, initial_value=False)
|
||||
|
||||
for trigger in self.config["trigger"]:
|
||||
self.shepherd.scheduler.add_job(self.camera_job, 'cron',
|
||||
hour=trigger["hour"],
|
||||
minute=trigger["minute"],
|
||||
second=trigger["second"])
|
||||
|
||||
def setup_other_modules(self):
|
||||
pass
|
||||
|
||||
def camera_job(self):
|
||||
self.interface.hook_pre_cam()
|
||||
|
||||
print("Running camera...")
|
||||
stream = io.BytesIO()
|
||||
with PiCamera() as picam:
|
||||
picam.resolution = (3280, 2464)
|
||||
picam.start_preview()
|
||||
time.sleep(2)
|
||||
picam.capture(stream, format='jpeg')
|
||||
# "Rewind" the stream to the beginning so we can read its content
|
||||
image_time = datetime.now()
|
||||
|
||||
stream.seek(0)
|
||||
newimage = Image.open(stream)
|
||||
try:
|
||||
fnt = ImageFont.truetype(overlayfont, 50)
|
||||
except IOError:
|
||||
fnt = ImageFont.load_default()
|
||||
|
||||
draw = ImageDraw.Draw(newimage)
|
||||
|
||||
image_text = image_time.strftime("%Y-%m-%d %H:%M:%S")
|
||||
if self.config["append_id"]:
|
||||
image_text = image_text + " " + self.shepherd.id
|
||||
image_text = image_text + self.config["append_text"]
|
||||
|
||||
draw.text((50, newimage.height-100), image_text, font=fnt,
|
||||
fill=(255, 255, 255, 200))
|
||||
|
||||
image_filename = image_time.strftime("%Y-%m-%d %H-%M-%S")
|
||||
if self.config["append_id"]:
|
||||
image_filename = image_filename + " " + self.shepherd.id
|
||||
|
||||
image_filename = image_filename + self.config["append_text"] + ".jpg"
|
||||
image_filename = os.path.join(self.save_directory, image_filename)
|
||||
newimage.save(image_filename+".writing", "JPEG")
|
||||
os.rename(image_filename+".writing", image_filename)
|
||||
|
||||
if self.config["upload_images"]:
|
||||
self.modules.uploader.move_to_bucket(image_filename,
|
||||
self.config["upload_bucket"])
|
||||
self.interface.hook_post_cam()
|
||||
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pass
|
||||
# print("main")
|
||||
# main(sys.argv[1:])
|
||||
@ -0,0 +1,210 @@
|
||||
import shutil
|
||||
import os
|
||||
import threading
|
||||
import paramiko
|
||||
import shepherd.config
|
||||
import shepherd.module
|
||||
# configdef = shepherd.config.definition()
|
||||
|
||||
# Can either import shepherd.config here, and call a function to build a config_def
|
||||
# or can leave a config_def entry point.
|
||||
# probably go with entry point, to stay consistent with the module
|
||||
|
||||
|
||||
class UploaderConfDef(shepherd.config.ConfDefinition):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
dests = self.add_def('destination', shepherd.config.TableArrayDef())
|
||||
dests.add_def('name', shepherd.config.StringDef())
|
||||
dests.add_def('protocol', shepherd.config.StringDef())
|
||||
dests.add_def('address', shepherd.config.StringDef(optional=True))
|
||||
dests.add_def('port', shepherd.config.IntDef(optional=True))
|
||||
dests.add_def('path', shepherd.config.StringDef(optional=True))
|
||||
dests.add_def('username', shepherd.config.StringDef(optional=True))
|
||||
dests.add_def('password', shepherd.config.StringDef(optional=True))
|
||||
dests.add_def('keyfile', shepherd.config.StringDef(default="", optional=True))
|
||||
dests.add_def('add_id_to_path', shepherd.config.BoolDef(default=True, optional=True))
|
||||
|
||||
buckets = self.add_def('bucket', shepherd.config.TableArrayDef())
|
||||
buckets.add_def('name', shepherd.config.StringDef())
|
||||
buckets.add_def('open_link_on_new', shepherd.config.BoolDef())
|
||||
buckets.add_def('opportunistic', shepherd.config.BoolDef(default=True, optional=True))
|
||||
buckets.add_def('keep_copy', shepherd.config.BoolDef())
|
||||
buckets.add_def('destination', shepherd.config.StringDef())
|
||||
|
||||
|
||||
# on server side, we want to be able to list commands that a module responds to
|
||||
# without actually instantiating the module class. Add command templates into
|
||||
# the conf_def, than attach to them in the interface? Was worried about having
|
||||
# "two sources of truth", but you already need to match the conf_def to the
|
||||
# name where you access the value in the module. Could have add_command, which
|
||||
# you then add standard conf_def subclasses to, to reuse validation and server
|
||||
# form generation logic...
|
||||
|
||||
|
||||
class UploaderInterface(shepherd.module.Interface):
|
||||
def __init__(self, module):
|
||||
super().__init__(module)
|
||||
# self.add_command("trigger", self.module.camera_job)
|
||||
def _get_bucket_path(self, bucket_name):
|
||||
return self._module.buckets[bucket_name].path
|
||||
|
||||
def move_to_bucket(self, filepath, bucket_name):
|
||||
dest_path = os.path.join(self._get_bucket_path(bucket_name),
|
||||
os.path.basename(filepath))
|
||||
temp_dest_path = dest_path + ".writing"
|
||||
shutil.move(filepath, temp_dest_path)
|
||||
os.rename(temp_dest_path, dest_path)
|
||||
|
||||
# notify bucket to check for new files
|
||||
self._module.buckets[bucket_name].newfile_event.set()
|
||||
|
||||
|
||||
class Destination():
|
||||
def __init__(self, config, core_interface):
|
||||
self.config = config
|
||||
self.shepherd = core_interface
|
||||
self.sendlist_condition = threading.Condition()
|
||||
self.send_list = []
|
||||
|
||||
self.thread = threading.Thread(target=self._send_files)
|
||||
self.thread.start()
|
||||
|
||||
# Override this in subclasses, implementing the actual upload process.
|
||||
# Return true on success, false on failure.
|
||||
def upload(self, filepath, suffix):
|
||||
print ("Dummy uploading "+filepath)
|
||||
return True
|
||||
|
||||
def add_files_to_send(self, file_path_list):
|
||||
self.sendlist_condition.acquire()
|
||||
for file_path in file_path_list:
|
||||
if file_path not in self.send_list:
|
||||
self.send_list.append(file_path)
|
||||
self.sendlist_condition.notify()
|
||||
|
||||
self.sendlist_condition.release()
|
||||
|
||||
def _file_available(self):
|
||||
return len(self.send_list) > 0
|
||||
|
||||
def _send_files(self):
|
||||
while True:
|
||||
self.sendlist_condition.acquire()
|
||||
# this drops through immediately if there is something to send, otherwise waits
|
||||
self.sendlist_condition.wait_for(self._file_available)
|
||||
file_to_send = self.send_list.pop(0)
|
||||
os.rename(file_to_send, file_to_send+".uploading")
|
||||
self.sendlist_condition.release()
|
||||
|
||||
# Rename uploaded file to end with ".uploaded" on success, or back
|
||||
# to original path on failure.
|
||||
try:
|
||||
self.upload(file_to_send, ".uploading")
|
||||
os.rename(file_to_send+".uploading", file_to_send+".uploaded")
|
||||
except:
|
||||
os.rename(file_to_send+".uploading", file_to_send)
|
||||
self.send_list.append(file_to_send)
|
||||
|
||||
|
||||
class SFTPDestination(Destination):
|
||||
def upload(self, filepath, suffix):
|
||||
with paramiko.Transport((self.config["address"],
|
||||
self.config["port"])) as transport:
|
||||
transport.connect(username=self.config["username"],
|
||||
password=self.config["password"])
|
||||
with paramiko.SFTPClient.from_transport(transport) as sftp:
|
||||
print("Uploading "+filepath+" to "+self.config["address"]+" via SFTP")
|
||||
if self.config["add_id_to_path"]:
|
||||
destdir = os.path.join(self.config["path"],
|
||||
self.shepherd.id)
|
||||
else:
|
||||
destdir = self.config["path"]
|
||||
|
||||
try:
|
||||
sftp.listdir(destdir)
|
||||
except IOError:
|
||||
print("Creating remot dir:" + destdir)
|
||||
sftp.mkdir(destdir)
|
||||
|
||||
print("Target dir:"+destdir)
|
||||
sftp.put(filepath+suffix,
|
||||
os.path.join(destdir, os.path.basename(filepath)))
|
||||
|
||||
|
||||
class Bucket():
|
||||
def __init__(self, name, open_link_on_new, opportunistic, keep_copy,
|
||||
destination, core_interface, path=None, old_path=None):
|
||||
self.newfile_event = threading.Event()
|
||||
self.newfile_event.set()
|
||||
|
||||
self.destination = destination
|
||||
|
||||
self.shepherd = core_interface
|
||||
self.path = path
|
||||
if self.path is None:
|
||||
self.path = os.path.join(self.shepherd.root_dir, name)
|
||||
if not os.path.exists(self.path):
|
||||
os.makedirs(self.path)
|
||||
|
||||
if keep_copy:
|
||||
self.old_path = old_path
|
||||
if self.old_path is None:
|
||||
self.old_path = os.path.join(self.shepherd.root_dir, name + "_old")
|
||||
if not os.path.exists(self.old_path):
|
||||
os.makedirs(self.old_path)
|
||||
|
||||
self.thread = threading.Thread(target=self._check_files)
|
||||
self.thread.start()
|
||||
|
||||
def _check_files(self):
|
||||
|
||||
while True:
|
||||
self.newfile_event.wait(timeout=10)
|
||||
self.newfile_event.clear()
|
||||
bucket_files = []
|
||||
for item in os.listdir(self.path):
|
||||
item_path = os.path.join(self.path, item)
|
||||
if (os.path.isfile(item_path) and
|
||||
(not item.endswith(".writing")) and
|
||||
(not item.endswith(".uploading")) and
|
||||
(not item.endswith(".uploaded"))):
|
||||
bucket_files.append(item_path)
|
||||
|
||||
if bucket_files:
|
||||
self.destination.add_files_to_send(bucket_files)
|
||||
|
||||
|
||||
class UploaderModule(shepherd.module.Module):
|
||||
conf_def = UploaderConfDef()
|
||||
|
||||
def __init__(self, config, core_interface):
|
||||
super().__init__(config, core_interface)
|
||||
|
||||
print("Uploader config:")
|
||||
print(self.config)
|
||||
|
||||
self.interface = UploaderInterface(self)
|
||||
|
||||
self.destinations = {}
|
||||
self.buckets = {}
|
||||
|
||||
for dest_conf in self.config["destination"]:
|
||||
if dest_conf["protocol"] == "sftp":
|
||||
self.destinations[dest_conf["name"]] = SFTPDestination(dest_conf, core_interface)
|
||||
else:
|
||||
self.destinations[dest_conf["name"]] = Destination(dest_conf, core_interface)
|
||||
|
||||
for bucketconf in self.config["bucket"]:
|
||||
bucketconf["destination"] = self.destinations[bucketconf["destination"]]
|
||||
self.buckets[bucketconf["name"]] = Bucket(
|
||||
**bucketconf, core_interface=self.shepherd)
|
||||
|
||||
def init_other_modules(self, interfaces): # pylint: disable=W0235
|
||||
super().init_other_modules(interfaces)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pass
|
||||
#print("main")
|
||||
#main(sys.argv[1:])
|
||||
Loading…
Reference in new issue