Package: regenmaschine
💧 Regenmaschine: A Simple Python Library for RainMachine™regenmaschine(German for "rain machine") is a simple, clean, well-tested
Python library for interacting withRainMachine™ smart sprinkler controllers. It gives developers an easy
API to manage their controllers over their local LAN or remotely via the RainMachine™
cloud.Remote Access AnnouncementPython VersionsInstallationUsageLoading Controllers Multiple TimesContributingRemote Access Announcement (2022-06-26)On June 2, 2022, RainMachine announced aPremium Servicesaddition; under this new model, remote access isonlyavailable to subscribers of
these Premium Services.I do not currently intend to subscribe to Premium Services; as such, the remote access
abilities ofregenmaschinewill remain as-is from here on out unless spurred on by
others. They may stop working at any time. PRs from subscribing users are always
welcome.Python Versionsregenmaschineis currently supported on:Python 3.10Python 3.11Python 3.12InstallationpipinstallregenmaschineUsageCreating aregenmaschineClientmight be the easiest thing you do all day:importasynciofromaiohttpimportClientSessionfromregenmaschineimportClientasyncdefmain()->None:"""Run!"""client=Client()# ...asyncio.run(main())By default, the library creates a new connection to the sprinkler controller with each
coroutine. If you are calling a large number of coroutines (or merely want to squeeze
out every second of runtime savings possible), anaiohttpClientSessioncan
be used for connection pooling:See the module docstrings throughout the library for full info on all parameters, return
types, etc.importasynciofromaiohttpimportClientSessionfromregenmaschineimportClientasyncdefmain()->None:"""Run!"""asyncwithClientSession()assession:client=Client(session=session)asyncio.run(main())Loading Local (Accessible Over the LAN) ControllersOnce you have a client, you can load a local controller (i.e., one that is
accessible over the LAN) very easily:importasynciofromaiohttpimportClientSessionfromregenmaschineimportClientasyncdefmain()->None:"""Run!"""asyncwithClientSession()assession:client=Client(session=session)awaitclient.load_local("192.168.1.101","my_password",port=8080,use_ssl=True)controllers=client.controllers# >>> {'ab:cd:ef:12:34:56': <LocalController>}asyncio.run(main())Loading Remote (Accessible Over the RainMachine Cloud) ControllersIf you have 1, 2 or 100 other local controllers, you can load them in the same
way –client.controllerswill keep your controllers all organized.What if you have controllers around the world and can't access them all over
the same local network? No problem!regenmaschineallows you to load remote
controllers very easily, as well:importasynciofromaiohttpimportClientSessionfromregenmaschineimportClientasyncdefmain()->None:"""Run!"""asyncwithClientSession()assession:client=Client(session=session)awaitclient.load_remote("rainmachine_email@host.com","my_password")controllers=client.controllers# >>> {'xx:xx:xx:xx:xx:xx': <RemoteController>, ...}asyncio.run(main())Bonus tip:client.load_remotewill loadallcontrollers owned by that email
address.Using the ControllerRegardless of the type of controller you have loaded (local or remote), the
same properties and methods are available to each:importasyncioimportdatetimefromaiohttpimportClientSessionfromregenmaschineimportClientasyncdefmain()->None:"""Run!"""asyncwithClientSession()assession:client=Client(session=session)# Load a local controller:awaitclient.load_local("192.168.1.101","my_password",port=8080,use_ssl=True)# Load all remote controllers associated with an account:awaitclient.load_remote("rainmachine_email@host.com","my_password")# They all act the same! The only difference is that remote API calls# will pass through the RainMachine™ cloud:formac_address,controllerinclient.controllers:# Print some client properties:print(f"Name:{controller.name}")print(f"Host:{controller.host}")print(f"MAC Address:{controller.mac}")print(f"API Version:{controller.api_version}")print(f"Software Version:{controller.software_version}")print(f"Hardware Version:{controller.hardware_version}")# Get all diagnostic information:diagnostics=awaitcontroller.diagnostics.current()# Get all weather parsers:parsers=awaitcontroller.parsers.current()# Get all programs:programs=awaitcontroller.programs.all()# Include inactive programs:programs=awaitcontroller.programs.all(include_inactive=True)# Get a specific program:program_1=awaitcontroller.programs.get(1)# Enable or disable a specific program:awaitcontroller.programs.enable(1)awaitcontroller.programs.disable(1)# Get the next run time for all programs:runs=awaitcontroller.programs.next()# Get all running programs:programs=awaitcontroller.programs.running()# Start and stop a program:awaitcontroller.programs.start(1)awaitcontroller.programs.stop(1)# Get basic details about all zones:zones=awaitcontroller.zones.all()# Get advanced details about all zones:zones=awaitcontroller.zones.all(details=True)# Include inactive zones:zones=awaitcontroller.zones.all(include_inactive=True)# Get basic details about a specific zone:zone_1=awaitcontroller.zones.get(1)# Get advanced details about a specific zone:zone_1=awaitcontroller.zones.get(1,details=True)# Enable or disable a specific zone:awaitcontroller.zones.enable(1)awaitcontroller.zones.disable(1)# Start a zone for 60 seconds:awaitcontroller.zones.start(1,60)# ...and stop it:awaitcontroller.zones.stop(1)# Get all running zones:programs=awaitcontroller.zones.running()# Get the device name:name=awaitcontroller.provisioning.device_name# Get all provisioning settings:settings=awaitcontroller.provisioning.settings()# Get all networking info related to the device:wifi=awaitcontroller.provisioning.wifi()# Get various types of active watering restrictions:current=awaitcontroller.restrictions.current()universal=awaitcontroller.restrictions.universal()hourly=awaitcontroller.restrictions.hourly()raindelay=awaitcontroller.restrictions.raindelay()# Set universal restrictions – note that the payload is the same structure# as returned by controller.restrictions.universal():awaitcontroller.restrictions.set_universal({"hotDaysExtraWatering":False,"freezeProtectEnabled":True,})# Get watering stats:today=awaitcontroller.stats.on_date(datetime.date.today())upcoming_days=awaitcontroller.stats.upcoming(details=True)# Get info on various watering activities not already covered:log=awaitcontroller.watering.log(datetime.date.today(),2)queue=awaitcontroller.watering.queue()runs=awaitcontroller.watering.runs(datetime.date.today())# Pause all watering activities for 30 seconds:awaitcontroller.watering.pause_all(30)# Unpause all watering activities:awaitcontroller.watering.unpause_all()# Stop all watering activities:awaitcontroller.watering.stop_all()# See if a firmware update is available:update_data=awaitcontroller.machine.get_firmware_update_status()# ...and request the update:update_data=awaitcontroller.machine.update_firmware()# Reboot the controller:update_data=awaitcontroller.machine.reboot()# Return the current flow meter data:flowmeter=awaitcontroller.watering.flowmeter()# Add values to flowmeter counters from an external smart water meter# not wired directly to the controller.# Units can be "clicks", "gal", "m3" and "litre".awaitcontroller.watering.post_flowmeter({"value":2000,"units":"clicks"})asyncio.run(main())Check outexample.py, the tests, and the source files themselves for method
signatures and more examples. For additional reference, the full RainMachine™ API
documentation is availablehere.Loading Controllers Multiple TimesIt is technically possible to load a controller multiple times. Let's pretend
for a moment that:We have a local controller namedHome(available at192.168.1.101).We have a remote controller namedGrandma's House.Both controllers live under our email address:user@host.comIf we load them thus:importasynciofromaiohttpimportClientSessionfromregenmaschineimportClientasyncdefmain()->None:"""Run!"""asyncwithClientSession()assession:client=Client(session=session)# Load "Home" locally:awaitclient.load_local("192.168.1.101","my_password")# Load all of my controllers remotely:awaitclient.load_remote("user@host.com","my_password")asyncio.run(main())...then we will have the following:Homewill be aLocalControllerand accessible over the LAN.Grandma's Housewill be aRemoteControllerand accessible only over the
RainMachine™ cloud.Notice thatregenmaschineis smart enough to not overwrite a controller that
already exists: even thoughHomeexists as a remote controller owned byuser@host.com, it had already been loaded locally. By default,regenmaschinewill only load a controller if it hasn't been loaded before
(locallyorremotely). If you want to change this behavior, bothload_localandload_remoteaccept an optionalskip_existingparameter:importasynciofromaiohttpimportClientSessionfromregenmaschineimportClientasyncdefmain()->None:"""Run!"""asyncwithClientSession()assession:client=Client(session=session)# Load all of my controllers remotely:awaitclient.load_remote("user@host.com","my_password")# Load "Home" locally, overwriting the existing remote controller:awaitclient.load_local("192.168.1.101","my_password",skip_existing=False)asyncio.run(main())ContributingThanks to all ofour contributorsso far!Check for open features/bugsorinitiate a discussion on one.Fork the repository.(optional, but highly recommended) Create a virtual environment:python3 -m venv .venv(optional, but highly recommended) Enter the virtual environment:source ./.venv/bin/activateInstall the dev environment:script/setupCode your new feature or bug fix on a new branch.Write tests that cover your new functionality.Run tests and ensure 100% code coverage:poetry run pytest --cov regenmaschine testsUpdateREADME.mdwith any new documentation.Submit a pull request!