Skip to main content

Dirigera Python Client

Test Pypi Downloads Downloads

This repository provides an unofficial Python client for controlling the IKEA Dirigera Smart Home Hub. Current features:

Support for other features will be added in the future and your input in form of issues and PRs is greatly appreciated.

Installation

pip install dirigera

Quickstart

  1. Find out the ip-address of your Dirigera (check your router)
  2. Once you installed dirigera with pip you can run the included generate-token script. Here you can directly set the ip-address of you dirigera as parameter.
    generate-token <Dirigera ip-address>
    
  3. The script starts the auth process. When prompted, you must push the action button on the bottom of your Dirigera.
  4. After that hit ENTER and your token will be printed to the console.
    Example:
    Press the action button on Dirigera then hit ENTER ...
    Your Token:
    mgwB.aXqwpzV89N0aUwBhZMJjD8a.UBPyzy2InGtqgwo2MO5.xX4ug7.uBcVJquwYzLnAijF7SdYKvNxTo0uzQKahV10A-3ZQOz-UAubGP6sHWt1CJx3QmWZyE7ZcMZKgODXjSzWL1lumKgGz5dUIwFi3rhNxgK-IsBGeGVhNXPt8vGrYEcZePwPvNAIg8RqmlH27L-JZPnkAtP2wHoOdW72Djot3yJsohtEsb0p9mJvoZFSavTlTr4LDuf584vuH5fha5xoR9QhhIvvgbAP-s4EHFqENNi6vrYLHKR.sdqnv4sYw6UH-l6oiPnnRLxinoqBPOlWhlcL9doFviXQE.tZ9X8WVqyBrd0NYHlo9iorEvUbnZuD02BEJrg4NLwgh3rZtyF0Mi46HenynzBohbPn4RnuSYYCiHt5EZnWedxBtDqc7mSTm1ZtyD
    
  5. Done. Use this token in the hub setup.
    dirigera.Hub(
       token="mgwB.aXqwpzV89N0aUwBhZMJjD8a...",
       ip_address="192.1..."
    )
    

Dirigera Hub

Setting up the client works by providing the token and ip address.

import dirigera

dirigera_hub = dirigera.Hub(
    token="mgwB.aXqwpzV89N0aUwBhZMJjD8a...",
    ip_address="192.1..."
)
List dirigera_hub functions
# Send a PATCH request to the hub
response = dirigera_hub.patch(route="/devices/1", data=[{"attributes": {"customName": "new name"}}])

# Send a GET request to the hub
response = dirigera_hub.get(route="/devices")

# Send a POST request to the hub
response = dirigera_hub.post(route="/scenes", data={"name": "new scene"})

# Send a DELETE request to the hub
response = dirigera_hub.delete(route="/scenes/1")

# Fetch all air purifiers registered in the hub
air_purifiers = dirigera_hub.get_air_purifiers()

# Fetch an air purifier by its name
air_purifier = dirigera_hub.get_air_purifier_by_name(air_purifier_name="living room air purifier")

# Fetch an air purifier by its ID
air_purifier = dirigera_hub.get_air_purifier_by_id(id_="1")

# Fetch all lights registered in the hub
lights = dirigera_hub.get_lights()

# Fetch a light by its name
light = dirigera_hub.get_light_by_name(lamp_name="kitchen light")

# Fetch a light by its ID
light = dirigera_hub.get_light_by_id(id_="1")

# Fetch all outlets registered in the hub
outlets = dirigera_hub.get_outlets()

# Fetch an outlet by its name
outlet = dirigera_hub.get_outlet_by_name(outlet_name="kitchen outlet")

# Fetch an outlet by its ID
outlet = dirigera_hub.get_outlet_by_id(id_="1")

# Fetch all environment sensors registered in the hub
sensors = dirigera_hub.get_environment_sensors()

# Fetch an environment sensor by its name
sensor = dirigera_hub.get_environment_sensor_by_name(environment_sensor_name="living room environment sensor")

# Fetch an environment sensor by its ID
sensor = dirigera_hub.get_environment_sensor_by_id(id_="1")

# Fetch all motion sensors registered in the hub
sensors = dirigera_hub.get_motion_sensors()

# Fetch a motion sensor by its name
light = dirigera_hub.get_motion_sensor_by_name(motion_sensor_name="kitchen motion sensor")

# Fetch a motion sensor by its ID
sensor = dirigera_hub.get_motion_sensor_by_id(id_="1")

# Fetch all open/close sensors registered in the hub
sensors = dirigera_hub.get_open_close_sensors()

# Fetch an open/close sensor by its name
sensor = dirigera_hub.get_open_close_sensor_by_name(open_close_sensor_name="front door sensor")

# Fetch an open/close sensor by its ID
sensor = dirigera_hub.get_open_close_by_id(id_="1")

# Fetch all blinds registered in the hub
blinds = dirigera_hub.get_blinds()

# Fetch a blind by its name
blind = dirigera_hub.get_blind_by_name(blind_name="living room blind")

# Fetch a blind by its ID
blind = dirigera_hub.get_blinds_by_id(id_="1")

# Fetch all controllers registered in the hub
controllers = dirigera_hub.get_controllers()

# Fetch a controller by its name
controller = dirigera_hub.get_controller_by_name(controller_name="remote control")

# Fetch a controller by its ID
controller = dirigera_hub.get_controller_by_id(id_="1")

# Fetch all scenes
scenes = dirigera_hub.get_scenes()

# Fetch a scene by its ID
scene = dirigera_hub.get_scene_by_id(scene_id="1")

# Fetch a scene by its name
scene = dirigera_hub.get_scene_by_name(scene_name="movie night")

# Fetch all adaptive profiles
profiles = dirigera_hub.get_adaptive_profiles()

# Fetch all water sensors registered in the hub
water_sensors = dirigera_hub.get_water_sensors()

# Fetch a water sensor by its name
water_sensor = dirigera_hub.get_water_sensor_by_name(water_sensor_name="bathroom water sensor")

# Fetch a water sensor by its ID
water_sensor = dirigera_hub.get_water_sensor_by_id(id_="1")

# Fetch all occupancy sensors registered in the hub
occupancy_sensors = dirigera_hub.get_occupancy_sensors()

# Fetch an occupancy sensor by its name
occupancy_sensor = dirigera_hub.get_occupancy_sensor_by_name(occupancy_sensor_name="hallway occupancy sensor")

# Fetch an occupancy sensor by its ID
occupancy_sensor = dirigera_hub.get_occupancy_sensor_by_id(id_="1")

# Fetch all light sensors registered in the hub
light_sensors = dirigera_hub.get_light_sensors()

# Fetch a light sensor by its name
light_sensor = dirigera_hub.get_light_sensor_by_name(light_sensor_name="living room light sensor")

# Fetch a light sensor by its ID
light_sensor = dirigera_hub.get_light_sensor_by_id(id_="1")

# Fetch all electrical sensors registered in the hub
electrical_sensors = dirigera_hub.get_electrical_sensors()

# Fetch an electrical sensor by its ID
electrical_sensor = dirigera_hub.get_electrical_sensor_by_id(id_="1")

# Fetch all devices registered in the hub
devices = dirigera_hub.get_all_devices()

# Create a new scene
scene = dirigera_hub.create_scene(
    info=Info(name="New Scene", icon=Icon.SCENES_BOOK),
    scene_type=SceneType.USER_SCENE,
    triggers=[],
    actions=[]
)

# Delete a scene by its ID
dirigera_hub.delete_scene(scene_id="1")

Devices

All available devices (Light, Controller, Outlet, ...) consist of the core data defined in device.py. Calling reload() on a device (or scene) fetches the current data from the hub and updates the object in place. It also returns the same object, so light = light.reload() works as well.

Core Device Data

id: str
relation_id: Optional[str] = None
type: str
device_type: str
created_at: datetime.datetime
is_reachable: bool
last_seen: datetime.datetime
attributes: Attributes
capabilities: Capabilities
room: Optional[Room] = None
device_set: List
remote_links: List[str]
is_hidden: Optional[bool] = None

Attributes

All devices have attributes. Some devices have special attributes (for example Light has `is_on``). These are the core attributes each device has:

custom_name: str
model: str
manufacturer: str
firmware_version: str
hardware_version: str
serial_number: Optional[str] = None
product_code: Optional[str] = None
ota_status: Optional[str] = None
ota_state: Optional[str] = None
ota_progress: Optional[int] = None
ota_policy: Optional[str] = None
ota_schedule_start: Optional[datetime.time] = None
ota_schedule_end: Optional[datetime.time] = None

Capabilities

All devices have capabilities (for some it is just empty lists). Capabilities desrcibe what send/receive actions can be performed:

can_send: List[str]
can_receive: List[str]

All devices have a room with the corresponging infos.

Room

id: str
name: str
color: str
icon: str

Controlling Lights

To get information about the available lights, you can use the get_lights() method:

lights = dirigera_hub.get_lights()

The light object has the following attributes (additional to the core attributes):

startup_on_off: Optional[StartupEnum] = None # Optional attributes are not present on all lights
is_on: bool
light_level: Optional[int] = None
color_temperature: Optional[int] = None
color_temperature_min: Optional[int] = None
color_temperature_max: Optional[int] = None
color_hue: Optional[float] = None
color_saturation: Optional[float] = None

Additionally the light has an adaptive_profile field (Optional[AdaptiveProfileState]) with the id and active state of the adaptive profile the light follows. It is empty (id and active are None) if the light is not in adaptive mode.

Available methods for light are:

# Reload the light data from the hub
light.reload()

# Set the name of the light
light.set_name(name="kitchen light 1")

# Turn the light on or off
light.set_light(lamp_on=True)

# Set the light level (1-100)
light.set_light_level(light_level=90)

# Set the color temperature
light.set_color_temperature(color_temp=3000)

# Set the light color using hue (0-360) and saturation (0.0-1.0)
light.set_light_color(hue=128, saturation=0.5)

# Set the startup behavior of the light
light.set_startup_behaviour(behaviour=StartupEnum.START_OFF)

# Let the light follow an adaptive profile (see Adaptive lighting below)
light.activate_adaptive_profile(profile_id="1")

Adaptive lighting

Adaptive profiles are created in the IKEA app and define a schedule of light levels and color temperatures over the day. After activate_adaptive_profile() the light follows this schedule, the hub interpolates between the entries of the schedule. Changing the light manually, for example by setting the light level, takes it out of adaptive mode again. Call light.reload() and check light.adaptive_profile.active to see if the light is still in adaptive mode.

profile = dirigera_hub.get_adaptive_profiles()[0]  # AdaptiveProfile with id and adaptive_schedule
light.activate_adaptive_profile(profile_id=profile.id)
light.reload()
print(light.adaptive_profile.active)  # True

Controlling Outlets

To get information about the available outlets, you can use the get_outlets() method:

outlets = dirigera_hub.get_outlets()

The outlet object has the following attributes (additional to the core attributes):

is_on: bool
startup_on_off: Optional[StartupEnum] = None
status_light: Optional[bool] = None
identify_period: Optional[int] = None
permitting_join: Optional[bool] = None
energy_consumed_at_last_reset: Optional[float] = None
current_active_power: Optional[float] = None
current_amps: Optional[float] = None
current_voltage: Optional[float] = None
total_energy_consumed: Optional[float] = None
total_energy_consumed_last_updated: Optional[datetime.datetime] = None
time_of_last_energy_reset: Optional[datetime.datetime] = None

Available methods for outlet are:

# Reload the outlet data from the hub
outlet.reload()

# Set the name of the outlet
outlet.set_name(name="kitchen socket 1")

# Turn the outlet on or off
outlet.set_on(outlet_on=True)

# Set the startup behavior of the outlet
outlet.set_startup_behaviour(behaviour=StartupEnum.START_OFF)

Controlling Air Purifier

To get information about the available air purifiers, you can use the get_air_purifiers() method:

air_purifiers = dirigera_hub.get_air_purifiers()

The air purifier object has the following attributes (additional to the core attributes):

fan_mode: FanModeEnum
fan_mode_sequence: str
motor_state: int
child_lock: bool
status_light: bool
motor_runtime: int
filter_alarm_status: bool
filter_elapsed_time: int
filter_lifetime: int
current_p_m25: int

Available methods for air purifier are:

# Reload the air purifier data from the hub
air_purifier.reload()

# Set the name of the air purifier
air_purifier.set_name(name="living room purifier")

# Set the fan mode of the air purifier
air_purifier.set_fan_mode(fan_mode=FanModeEnum.AUTO)

# Set the motor state of the air purifier (0-50)
air_purifier.set_motor_state(motor_state=42)

# Enable or disable the child lock
air_purifier.set_child_lock(child_lock=True)

# Turn the status light on or off
air_purifier.set_status_light(light_state=False)

Controlling Blinds

To get information about the available blinds, you can use the get_blinds() method:

blinds = dirigera_hub.get_blinds()

The blind object has the following attributes (additional to the core attributes):

blinds_current_level: Optional[int] = None
blinds_target_level: Optional[int] = None
blinds_state: Optional[str] = None
battery_percentage: Optional[int] = None

Available methods for blinds are:

# Reload the blind data from the hub
blind.reload()

# Set the name of the blind
blind.set_name(name="kitchen blind 1")

# Set the target level of the blind (0-100)
blind.set_target_level(target_level=90)

# Move the blind up or down, or stop it at its current position
blind.set_state(state=BlindsStateEnum.STOPPED)

Remote Controllers

Currently only tested with the STYRBAR remote.

To get information about the available controllers, you can use the get_controllers() method:

controllers = dirigera_hub.get_controllers()

The controller object has the following attributes (additional to the core attributes):

is_on: Optional[bool] = None
battery_percentage: Optional[int] = None
switch_label: Optional[str] = None

Available methods for controller are:

# Reload the controller data from the hub
controller.reload()

# Set the name of the controller
controller.set_name(name="kitchen remote 1")

Environment Sensor

Currently only tested with the VINDSTYRKA sensor. If you have other sensors please send me the json and I will add support or create a PR.

To get the environment sensors use:

sensors = dirigera_hub.get_environment_sensors()

The environment sensor object has the following attributes (additional to the core attributes):

current_temperature: Optional[float] = None
current_r_h: Optional[int] = None  # current humidity
current_p_m25: Optional[int] = None # current particulate matter 2.5
current_c_o2: Optional[int] = None # current carbon dioxide in ppm, only for Alpstuga Air Quality Sensor
max_measured_p_m25: Optional[int] = None # maximum measurable particulate matter 2.5
min_measured_p_m25: Optional[int] = None # minimum measurable particulate matter 2.5
voc_index: Optional[int] = None # current volatile organic compound, only for Vindstyrka Air Quality Sensor

Available methods for environment sensor are:

# Reload the environment sensor data from the hub
sensor.reload()

# Set the name of the environment sensor
sensor.set_name(name="Bathroom Sensor")

Scene

To get the scenes use:

scenes = dirigera_hub.get_scenes()

The scene object has the following attributes:

id: str
type: SceneType
info: Info
triggers: List[Trigger]
actions: List[Action]
created_at: datetime.datetime
last_completed: Optional[datetime.datetime] = None
last_triggered: Optional[datetime.datetime] = None
last_undo: Optional[datetime.datetime] = None
commands: List[Union[str, Dict[str, Any]]]
undo_allowed_duration: int

Details to the Trigger, Action and Info class can be found in scene.py

Available methods for scene are:

scene.reload()
scene.trigger()
scene.undo()

Creating a Scene

To create a scene use the create_scene() function.
Example how to create an empty scene:

scene = dirigera_hub.create_scene(
    info=Info(name="This is empty", icon=Icon.SCENES_BOOK),
)

Actions look like this:

class Action(BaseIkeaModel):
    id: str
    type: str
    enabled: Optional[bool] = None
    attributes: Optional[ActionAttributes] = None

Example how create scene with action:

from dirigera.devices.scene import Info, Icon, SceneType, Action, ActionAttributes

light = dirigera_hub.get_light_by_name("kitchen_lamp")

scene = dirigera_hub.create_scene(
    info=Info(name="Scene with action", icon=Icon.SCENES_BOOK),
    scene_type=SceneType.USER_SCENE,
    triggers=[],
    actions=[Action(id=light.id, type="device", enabled=True, attributes=ActionAttributes(is_on=False))],
)

Triggers look like this:

class Trigger(BaseIkeaModel):
    id: Optional[str] = None
    type: str
    triggered_at: Optional[datetime.datetime] = None
    disabled: bool
    trigger: Optional[TriggerDetails] = None

Example how to create scene with trigger:

from dirigera.devices.scene import Info, Icon, Trigger, SceneType, TriggerDetails, ControllerType, ClickPattern

scene = dirigera_hub.create_scene(
   info=Info(name="Scene with trigger", icon=Icon.SCENES_HEART),
   scene_type=SceneType.USER_SCENE,
   triggers=[
       Trigger(type="app", disabled=False),
       Trigger(type="controller", disabled=False,
               trigger=TriggerDetails(clickPattern=ClickPattern.SINGLE_PRESS, buttonIndex=0,
                                      deviceId="0000aaaa-0000-0000-aa00-0a0aa0a000a0_1",
                                      controllerType=ControllerType.SHORTCUT_CONTROLLER))])

All available icons can be found here: Icons

Motion Sensor

To get information about the available motion sensors, you can use the get_motion_sensors() method:

motion_sensors = dirigera_hub.get_motion_sensors()

The motion sensor object has the following attributes (additional to the core attributes):

battery_percentage: Optional[int] = None
is_on: bool
light_level: Optional[float] = None
is_detected: Optional[bool] = False

Available methods for motion sensor are:

# Reload the motion sensor data from the hub
motion_sensor.reload()

# Set the name of the motion sensor
motion_sensor.set_name(name="kitchen sensor 1")

Open Close Sensor

To get information about the available open/close sensors, you can use the get_open_close_sensors() method:

open_close_sensors = dirigera_hub.get_open_close_sensors()

The hub does not guarantee the order of the returned devices, so look sensors up by their id (get_open_close_by_id()) or name (get_open_close_sensor_by_name()) instead of by their position in the list.

The open_close_sensor object has the following attributes (additional to the core attributes):

is_open: bool
battery_percentage: Optional[int] = None

Available methods for open/close sensor are:

# Reload the open/close sensor data from the hub
open_close_sensor.reload()

# Set the name of the open/close sensor
open_close_sensor.set_name(name="window 1")

Electrical Sensor

Plugs with energy metering, like the GRILLPLATS plug, are reported by the hub as two devices that share the same relation_id: an outlet (isOn, startup behaviour, ...) and an electrical sensor that holds the metering values.

To get the electrical sensors use:

electrical_sensors = dirigera_hub.get_electrical_sensors()

The electrical sensor object has the following attributes (additional to the core attributes):

current_active_power: Optional[float] = None
current_amps: Optional[float] = None
current_voltage: Optional[float] = None
energy_consumed_at_last_reset: Optional[float] = None
total_energy_consumed: Optional[float] = None
total_energy_consumed_last_updated: Optional[datetime.datetime] = None
time_of_last_energy_reset: Optional[datetime.datetime] = None

Available methods for electrical sensor are:

# Reload the electrical sensor data from the hub
electrical_sensor.reload()

# Set the name of the electrical sensor
electrical_sensor.set_name(name="grill plug")

To find the electrical sensor that belongs to an outlet, compare their relation_id:

outlet = dirigera_hub.get_outlet_by_name(outlet_name="grill plug")
electrical_sensor = next(
    sensor
    for sensor in dirigera_hub.get_electrical_sensors()
    if sensor.relation_id == outlet.relation_id
)
print(electrical_sensor.attributes.current_active_power)

Event Listener

The event listener allows you to listen to events that are published by your Dirigera hub. This is useful if you want to automate tasks based on events such as when a light is turned on or off, or when the color temperature of a light is changed.

import json
from typing import Any


def on_message(ws: Any, message: str):
    message_dict = json.loads(message)
    data = message_dict["data"]
    if data["id"] == bed_light.id:
        print(f"{message_dict['type']} event on {bed_light.attributes.custom_name}, attributes: {data['attributes']}")

def on_error(ws: Any, message: str):
    print(message)

dirigera_hub.create_event_listener(
    on_message=on_message, on_error=on_error
)
deviceStateChanged event on Bed Light, attributes: {'isOn': False}

Reconnecting

The hub can close the connection on its own (status 1000, "disconnected due to inactivity."). When that happens, create_event_listener() returns. The reconnect argument doesn't restart it, since it only covers connection errors and not a clean close by the hub. To keep listening, call it in a loop:

import time

running = True

def stop_listening():
    # create_event_listener() blocks, so call this from another thread
    global running
    running = False
    dirigera_hub.stop_event_listener()

while running:
    dirigera_hub.create_event_listener(on_message=on_message, on_error=on_error)
    time.sleep(5)  # short backoff before listening again

Events that happen while the listener is reconnecting are missed, so re-fetch the device state after reconnecting if you need it (for example in on_open).

Motivation

The primary motivation for this project was to provide users with the ability to control the startup behavior of their smart home lamps when there is a power outage.
The default behavior of the hub is to turn on all lights when power is restored, which can be problematic if the user is away from home or on vacation, and a small power fluctuation causes all lights to turn on and stay on. Unfortunately, the IKEA app does not offer a way to change this default behavior.
The set_startup_behaviour() function enables users to override the default behavior and choose the startup behavior that best suits their needs (START_ON = turn on light when power is back, START_OFF = light stays off when power is back).
I can not guarantee that all IKEA lamps offer this functionality. EDIT: This is now an exposed feature in the app.

Contributing

Contributions are welcome! If you have an idea for a new feature or a bug fix, please post and issue or submit a pull request.

Setup of dev

For setting up the dev environment I recommend running the setup.sh script, which will create a venv and install the requirements.txt as well as the dev-requirements.txt.

Tests

To run the tests execute the run-test.sh script or just run pytest .
For linting you can run the run-pylint.sh.
For types you can run the run-mypy.sh
To test the different python versions you can use the run-python-verions-test.sh (this requires a running docker installation).
All of these tests are also run when a PR is openend (and the test run is triggered).

License

The MIT License (MIT)

Copyright (c) 2023 Leggin

Metadata

Release files for dirigera 1.4.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for dirigera 1.4.0
File Size Uploaded
dirigera-1.4.0.tar.gz 44.1 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for dirigera 1.4.0
File Interpreter ABI Platform
dirigera-1.4.0-py3-none-any.whl Python 3 none any Details

Total release size: 75.4 kB

Release files / dirigera-1.4.0.tar.gz

Download URL dirigera-1.4.0.tar.gz
Size 44.1 kB
Tags Source
SHA-256 checksum
How to use checksums
4f4830cf5f9e81b91d2858aa6fa54a137e78deaf5cc7bbbf9756ed20b52ba1e8
BLAKE2b-256 checksum
How to use checksums
5d6222b32c76d2c800e0f418c2bcb67fcc574282e9b25778445e3268761f4084
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.14

Release files / dirigera-1.4.0-py3-none-any.whl

Download URL dirigera-1.4.0-py3-none-any.whl
Size 31.3 kB
Tags Python 3
SHA-256 checksum
How to use checksums
a0040084a26a8f26956707349d52be0ee3f62bac609a70b67d75141abc7108f1
BLAKE2b-256 checksum
How to use checksums
7445c1e2de4fdd7ea45ad9a7bb7da27a8ee003371fa015a8f082f6eab3850475
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.14

Release history Release notifications | RSS feed

1.4.1

2 release files

This release

1.4.0 This release

2 release files

1.3.1

2 release files

1.3.0

2 release files

1.2.7

2 release files

1.2.6

2 release files

1.2.5

2 release files

1.2.4

2 release files

1.2.3

2 release files

1.2.2

2 release files

1.2.1

2 release files

1.2.0

2 release files

1.1.9

2 release files

1.1.8

2 release files

1.1.7

2 release files

1.1.6

2 release files

1.1.5

2 release files

1.1.4

2 release files

1.1.3

2 release files

1.1.2

2 release files

1.1.1

2 release files

1.1.0

2 release files

1.0.12

2 release files

1.0.11

2 release files

1.0.10

2 release files

1.0.9

2 release files

1.0.8

2 release files

1.0.7

2 release files

1.0.6

2 release files

1.0.5

2 release files

1.0.4

2 release files

1.0.3

2 release files

1.0.2

2 release files

1.0.1

2 release files

1.0.0

2 release files

0.1.11

2 release files

0.1.10

2 release files

0.1.9

2 release files

0.1.8

2 release files

0.1.7

2 release files

0.1.6

2 release files

0.1.5

2 release files

0.1.4

2 release files

0.1.3

2 release files

0.1.2

2 release files

0.1.1

2 release files

0.1.0

2 release files

0.0.3

2 release files

0.0.2

2 release files

0.0.1

2 release files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page