September 27th, 2025 Update
@@ -3,6 +3,14 @@ name: Compile FrogPilot
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
runner:
|
||||
description: "Select the runner"
|
||||
required: true
|
||||
default: "c3"
|
||||
type: choice
|
||||
options:
|
||||
- c3
|
||||
- c3x
|
||||
not_vetted:
|
||||
description: "This branch is not vetted"
|
||||
required: false
|
||||
@@ -46,7 +54,9 @@ env:
|
||||
|
||||
jobs:
|
||||
get_branch:
|
||||
runs-on: [self-hosted, c3x]
|
||||
runs-on:
|
||||
- self-hosted
|
||||
- ${{ github.event.inputs.runner }}
|
||||
outputs:
|
||||
branch: ${{ steps.get_branch.outputs.branch }}
|
||||
python_version: ${{ steps.get_python_version.outputs.python_version }}
|
||||
@@ -108,7 +118,9 @@ jobs:
|
||||
|
||||
- name: Vet Existing Translations
|
||||
if: github.event.inputs.vet_existing_translations == 'true'
|
||||
continue-on-error: true
|
||||
run: python selfdrive/ui/translations/auto_translate.py --all-files --vet-translations
|
||||
timeout-minutes: 300
|
||||
|
||||
- name: Commit and Push Translation Updates
|
||||
run: |
|
||||
@@ -128,7 +140,9 @@ jobs:
|
||||
- get_branch
|
||||
- translate
|
||||
if: always()
|
||||
runs-on: [self-hosted, c3x]
|
||||
runs-on:
|
||||
- self-hosted
|
||||
- ${{ github.event.inputs.runner }}
|
||||
permissions:
|
||||
contents: write
|
||||
defaults:
|
||||
@@ -185,19 +199,12 @@ jobs:
|
||||
rm -f panda/board/obj/version
|
||||
|
||||
find . -name '*.a' -delete
|
||||
find . -name '*.cc' -delete
|
||||
find . -name '*.o' -delete
|
||||
find . -name '*.onnx' -delete
|
||||
find . -name '*.os' -delete
|
||||
find . -name '*.pyc' -delete
|
||||
find . -name 'moc_*' -delete
|
||||
|
||||
find . -name '*.h' | while read -r header; do
|
||||
if [[ "$header" != *"common/version.h" && "$header" != *"system/camerad/sensors/"* ]]; then
|
||||
rm -f "$header"
|
||||
fi
|
||||
done
|
||||
|
||||
rm -rf .devcontainer/
|
||||
rm -rf .vscode/
|
||||
rm -rf body/
|
||||
@@ -215,7 +222,7 @@ jobs:
|
||||
-name 'review_pull_request.yaml' -o \
|
||||
-name 'schedule_update.yaml' -o \
|
||||
-name 'update_pr_branch.yaml' -o \
|
||||
-name 'update_release_branch.yaml' \
|
||||
-name 'update_release_branch.yaml' -o \
|
||||
-name 'update_tinygrad.yaml' \
|
||||
\) \
|
||||
\) -exec rm -rf {} +
|
||||
|
||||
@@ -69,21 +69,25 @@ jobs:
|
||||
- name: Update README Date and Remove update file
|
||||
run: |
|
||||
DAY=$(TZ="${{ env.TZ }}" date +'%d' | sed 's/^0//')
|
||||
|
||||
case "$DAY" in
|
||||
1|21|31) SUFFIX="st" ;;
|
||||
2|22) SUFFIX="nd" ;;
|
||||
3|23) SUFFIX="rd" ;;
|
||||
*) SUFFIX="th" ;;
|
||||
2|22) SUFFIX="nd" ;;
|
||||
3|23) SUFFIX="rd" ;;
|
||||
*) SUFFIX="th" ;;
|
||||
esac
|
||||
|
||||
MONTH=$(TZ="${{ env.TZ }}" date +'%B')
|
||||
YEAR=$(TZ="${{ env.TZ }}" date +'%Y')
|
||||
NEW_DATE="**${MONTH} ${DAY}${SUFFIX}, ${YEAR}**"
|
||||
|
||||
sed -i "/FrogPilot was last updated on:/ { N; N; s/\(\n\)\n.*$/\1\n${NEW_DATE}/; }" README.md
|
||||
DATE="${MONTH} ${DAY}${SUFFIX}, ${YEAR}"
|
||||
DATE_ESCAPED=$(printf '%s' "$DATE" | sed -E 's/ /%20/g; s/,/%2C/g')
|
||||
|
||||
sed -i -E "s|(Last%20Updated-)[^-)]*|\1${DATE_ESCAPED}|g" README.md
|
||||
|
||||
git add README.md
|
||||
git rm -f "${{ env.UPDATE_FILE }}"
|
||||
git commit -m "Updated README date to ${NEW_DATE}"
|
||||
git commit -m "Updated README date to ${DATE}"
|
||||
|
||||
- name: Squash Commits
|
||||
run: |
|
||||
|
||||
@@ -2,11 +2,22 @@ name: Update Tinygrad
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
runner:
|
||||
description: "Select the runner"
|
||||
required: true
|
||||
default: "c3"
|
||||
type: choice
|
||||
options:
|
||||
- c3
|
||||
- c3x
|
||||
|
||||
jobs:
|
||||
update_tinygrad:
|
||||
name: Update Tinygrad
|
||||
runs-on: [self-hosted, c3x]
|
||||
runs-on:
|
||||
- self-hosted
|
||||
- ${{ github.event.inputs.runner }}
|
||||
|
||||
steps:
|
||||
- name: Get version
|
||||
@@ -14,32 +25,43 @@ jobs:
|
||||
run: |
|
||||
VERSION=$(grep -oP '^VERSION\s*=\s*"\K[^"]+' /data/openpilot/frogpilot/assets/model_manager.py)
|
||||
echo "VERSION=$VERSION"
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
|
||||
|
||||
- name: Clone GitLab repo
|
||||
env:
|
||||
GITLAB_TOKEN: ${{ secrets.GITLAB_TOKEN }}
|
||||
run: |
|
||||
rm -rf /data/tmp/frogpilot_tinygrad
|
||||
mkdir -p /data/tmp/frogpilot_tinygrad
|
||||
cd /data/tmp/frogpilot_tinygrad
|
||||
git clone --depth 1 --branch Tinygrad https://oauth2:${GITLAB_TOKEN}@gitlab.com/FrogAi/FrogPilot-Resources.git
|
||||
|
||||
git clone --depth 1 --branch Tinygrad "https://oauth2:${GITLAB_TOKEN}@gitlab.com/FrogAi/FrogPilot-Resources.git"
|
||||
|
||||
- name: Create Tinygrad Archive
|
||||
run: |
|
||||
set -euo pipefail
|
||||
cd /data/openpilot
|
||||
|
||||
ARCHIVE_DIR="/data/tmp/frogpilot_tinygrad/FrogPilot-Resources"
|
||||
ARCHIVE_NAME="Tinygrad_${{ steps.get_version.outputs.version }}.tar.gz"
|
||||
|
||||
tar -czf "/data/tmp/frogpilot_tinygrad/FrogPilot-Resources/$ARCHIVE_NAME" \
|
||||
TMPDIR="$(mktemp -d)"
|
||||
touch "$TMPDIR/SConscript"
|
||||
|
||||
tar -czf "$ARCHIVE_NAME" \
|
||||
--exclude="*.a" \
|
||||
--exclude="*.cc" \
|
||||
--exclude="*.h" \
|
||||
--exclude="*.o" \
|
||||
--exclude="*.onnx" \
|
||||
--exclude="*.pkl" \
|
||||
--exclude="*__pycache__*" \
|
||||
--exclude="*tests*" \
|
||||
frogpilot/tinygrad_modeld tinygrad tinygrad_repo
|
||||
--exclude="frogpilot/tinygrad_modeld/SConscript" \
|
||||
frogpilot/tinygrad_modeld tinygrad_repo \
|
||||
-C "$TMPDIR" \
|
||||
--transform 's|^SConscript$|frogpilot/tinygrad_modeld/SConscript|' \
|
||||
SConscript
|
||||
|
||||
mv "$ARCHIVE_NAME" "$ARCHIVE_DIR/"
|
||||
rm -rf "$TMPDIR"
|
||||
|
||||
- name: Push updated Tinygrad
|
||||
run: |
|
||||
@@ -49,7 +71,7 @@ jobs:
|
||||
git config user.email "91348155+FrogAi@users.noreply.github.com"
|
||||
|
||||
git add Tinygrad_*.tar.gz
|
||||
git commit -m "Updated Tinygrad: ${{ steps.get_version.outputs.version }}"
|
||||
git commit -m "Updated Tinygrad: ${{ steps.get_version.outputs.version }}" || echo "No changes to commit"
|
||||
git push origin Tinygrad
|
||||
|
||||
- name: Cleanup temporary files
|
||||
|
||||
@@ -1,9 +1,33 @@
|
||||
[](https://comma.ai/shop/comma-3x)
|
||||
<div align="center" style="text-align: center;">
|
||||
|
||||
What is openpilot?
|
||||
------
|
||||
<h1>openpilot</h1>
|
||||
|
||||
[openpilot](http://github.com/commaai/openpilot) is an open source driver assistance system. Currently, openpilot performs the functions of Adaptive Cruise Control (ACC), Automated Lane Centering (ALC), Forward Collision Warning (FCW), and Lane Departure Warning (LDW) for a growing variety of [supported car makes, models, and model years](docs/CARS.md). In addition, while openpilot is engaged, a camera-based Driver Monitoring (DM) feature alerts distracted and asleep drivers. See more about [the vehicle integration](docs/INTEGRATION.md) and [limitations](docs/LIMITATIONS.md).
|
||||
<p>
|
||||
<b>openpilot is an operating system for robotics.</b>
|
||||
<br>
|
||||
Currently, it upgrades the driver assistance system in 300+ supported cars.
|
||||
</p>
|
||||
|
||||
<h3>
|
||||
<a href="https://docs.comma.ai">Docs</a>
|
||||
<span> · </span>
|
||||
<a href="https://docs.comma.ai/contributing/roadmap/">Roadmap</a>
|
||||
<span> · </span>
|
||||
<a href="https://github.com/commaai/openpilot/blob/master/docs/CONTRIBUTING.md">Contribute</a>
|
||||
<span> · </span>
|
||||
<a href="https://discord.comma.ai">Community</a>
|
||||
<span> · </span>
|
||||
<a href="https://comma.ai/shop">Try it on a comma 3X</a>
|
||||
</h3>
|
||||
|
||||
Quick start: `bash <(curl -fsSL openpilot.comma.ai)`
|
||||
|
||||
[](https://github.com/commaai/openpilot/actions/workflows/selfdrive_tests.yaml)
|
||||
[](LICENSE)
|
||||
[](https://x.com/comma_ai)
|
||||
[](https://discord.comma.ai)
|
||||
|
||||
</div>
|
||||
|
||||
<table>
|
||||
<tr>
|
||||
@@ -13,217 +37,216 @@ What is openpilot?
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
What is FrogPilot? 🐸
|
||||
|
||||
Using openpilot in a car
|
||||
------
|
||||
|
||||
FrogPilot is a fully open-sourced fork of openpilot, featuring clear and concise commits striving to be a resource for the openpilot developer community. It thrives on contributions from both users and developers, focusing on a collaborative, community-led approach to deliver an advanced openpilot experience for everyone!
|
||||
To use openpilot in a car, you need four things:
|
||||
1. **Supported Device:** a comma 3/3X, available at [comma.ai/shop](https://comma.ai/shop/comma-3x).
|
||||
2. **Software:** The setup procedure for the comma 3/3X allows users to enter a URL for custom software. Use the URL `openpilot.comma.ai` to install the release version.
|
||||
3. **Supported Car:** Ensure that you have one of [the 275+ supported cars](docs/CARS.md).
|
||||
4. **Car Harness:** You will also need a [car harness](https://comma.ai/shop/car-harness) to connect your comma 3/3X to your car.
|
||||
|
||||
We have detailed instructions for [how to install the harness and device in a car](https://comma.ai/setup). Note that it's possible to run openpilot on [other hardware](https://blog.comma.ai/self-driving-car-for-free/), although it's not plug-and-play.
|
||||
|
||||
------
|
||||
FrogPilot was last updated on:
|
||||
|
||||
**April 12th, 2025**
|
||||
<div align="center" style="text-align: center;">
|
||||
|
||||
Features
|
||||
------
|
||||
<h1>FrogPilot 🐸</h1>
|
||||
|
||||
FrogPilot offers a wide range of customizable features that are easily toggled on or off to suit your preferences. Whether you want a completely stock openpilot experience, or want to add some fun and personal touches, FrogPilot has you covered! Some of the features include:
|
||||
[](https://deepwiki.com/FrogAi/FrogPilot)
|
||||
[](https://discord.frogpilot.download)
|
||||
[](https://github.com/FrogAi/FrogPilot/releases/latest)
|
||||
[](https://frogpilot.wiki.gg/)
|
||||
|
||||
</div>
|
||||
|
||||
------
|
||||
⚡ **Advanced Customizations:**
|
||||
|
||||
- "Alert Volume Controller" to set the volume level for each of of openpilot's sounds
|
||||
- Customize the following distance and jerk values for each personality profile
|
||||
- Fine tune your car's "Steer Ratio" to perfect your lateral control
|
||||
- Increase the distance when stopped behind lead vehicles
|
||||
- Increase the max set speed by a custom interval (i.e. 2, 3, 4, 5, 6, etc. instead of just 1)
|
||||
- Select between past, present, and future openpilot driving models
|
||||
------
|
||||
🎨 **Custom Themes:**
|
||||
**FrogPilot** is a custom frog-themed fork of openpilot that embraces a collaborative, community-driven approach to push the project forward. It delivers bleeding-edge features and experimental improvements far ahead of official releases. As an unofficial and highly experimental version of openpilot, **FrogPilot** should always be used with caution!
|
||||
|
||||
- 🐸 Frog theme (with a bonus 🐐 sound effect)
|
||||
- <img src="https://images.emojiterra.com/google/noto-emoji/unicode-15/color/512px/1f1f7-1f1fa.png" width="15" height="12"> Russia / Joseph Stalin theme
|
||||
- 🔌 Tesla theme
|
||||
- Holiday themes! Minor holidays last a day, while major holidays (Easter, Halloween, Thanksgiving, Christmas) last a week
|
||||
- Random events triggered by specific actions while driving with openpilot
|
||||
|
||||
- 📢 Want to add a theme? Request one in the "feature-request" channel in the FrogPilot Discord!
|
||||
------
|
||||
🚀 **Conditional Experimental Mode:**
|
||||
|
||||
- Auto-activates "Experimental Mode" under several conditions, including:
|
||||
- Approaching intersections and turns while using navigation
|
||||
- Approaching slower vehicles to take advantage of "Experimental Mode"'s smoother braking
|
||||
- Curve and stop light/stop sign detection
|
||||
- Driving below a set speed
|
||||
- Turn signal activation below 55mph for turn assistance
|
||||
------
|
||||
📊 **Developer UI:**
|
||||
|
||||
- Display various driving logics such as the distance, speed, and the desired following distance to your lead vehicle
|
||||
- Lane measuring of the adjacent lanes for lane detection
|
||||
- Tap the "VEHICLE ONLINE"/"CPU"/"GPU" gauge to toggle between CPU and GPU monitoring
|
||||
- Tap the "CONNECT ONLINE"/"MEMORY"/"LEFT"/"USED" gauge to toggle between RAM and storage monitoring
|
||||
------
|
||||
🛠 **Device Management:**
|
||||
|
||||
- Adjustable screen brightness for both onroad and offroad states
|
||||
- Adjustable screen timeout times for both onroad and offroad states
|
||||
- Backup and restore previous versions of FrogPilot
|
||||
- Backup and restore previous versions of toggle configurations
|
||||
- Battery level threshold to automatically shut the device down after you car's battery falls below a set voltage limit when offroad
|
||||
- Delete stored driving data for increased privacy/space via the "Device" panel
|
||||
- Device can operate offline indefinitely
|
||||
- Disable logging and/or uploading
|
||||
- Disable uploads while onroad to help reduce data usage
|
||||
- Flash the Panda within the "Device" menu
|
||||
- "Standby Mode" that wakes the screen up between engagement states or when important alerts are triggered
|
||||
- Timer to automatically shut down after going offroad
|
||||
------
|
||||
🚖 **Lateral Adjustments:**
|
||||
|
||||
- Activate lateral control by simply pressing the "Cruise Control" button
|
||||
- Force comma's auto tuning for unsupported vehicles
|
||||
- Lateral control won't disengage on gas or brake
|
||||
- Nudgeless lane changes with lane detection to prevent driving into curbs or going offroad
|
||||
- Pause lateral control when below a set speed
|
||||
- Pause lateral control when pressing the brake
|
||||
- Pause lateral control when turn signals are active
|
||||
- Precise turns by using turn desires when below the minimum lane change speed
|
||||
- [Twilsonco's NNFF](https://github.com/twilsonco/openpilot) for smoother steering control
|
||||
------
|
||||
🚘 **Longitudinal Adjustments:**
|
||||
|
||||
- Aggressive acceleration when following a lead vehicle from a stop
|
||||
- "Map Turn Speed Controller" to slow down for curves based on stored map data
|
||||
- With an additional toggle to fine tune the speed aggressiveness
|
||||
- Smoother braking behind slower leads
|
||||
- "Speed Limit Controller" to adjust your speed to the posted speed limit
|
||||
- With additional toggles to set offsets for "0-34 mph", "35-54 mph", "55-64 mph", and "65-99 mph"
|
||||
- "Sport" and "Eco" acceleration and deceleration profiles
|
||||
- "Traffic Mode" tailored towards driving in traffic
|
||||
- Tweak the lead detection threshold to detect leads sooner for smoother braking on stopped/slower vehicles
|
||||
- "Vision Turn Speed Controller" for smoother handling of curves
|
||||
- With additional toggles to fine tune the speed aggressiveness and curve detection sensitivity
|
||||
------
|
||||
🗺️ **Navigation:**
|
||||
|
||||
- 3D buildings
|
||||
- Custom map styles
|
||||
- Full screen map that takes up the entire screen for a more expansive map view
|
||||
- iOS shortcuts to quickly set navigation destinations
|
||||
- Navigate on openpilot without a comma prime subscription
|
||||
- Offline maps
|
||||
- "Open Street Maps" integration for speed limit control and road name view
|
||||
------
|
||||
🎮 **Onroad UI:**
|
||||
|
||||
- Compass that rotates according to the direction you're driving
|
||||
- FPS counter in the screen's border
|
||||
- Hide various UI elements on the screen for a cleaner UI
|
||||
- Pedals on the onroad UI indicate when the gas/brake pedals are being used
|
||||
- Road UI Customizations:
|
||||
- Acceleration path to show the model's desired acceleration/deceleration
|
||||
- Blind spot path when a vehicle is detected in your blind spot
|
||||
- Increase/decrease the lane line, path, and road edge widths
|
||||
- Path edge colors based on specific driving statuses:
|
||||
- 🔵 Blue - Navigation active
|
||||
- 🟦 Light Blue - "Always On Lateral" active
|
||||
- 🟢 Green - Default
|
||||
- 🟠 Orange - "Experimental Mode" active
|
||||
- 🔴 Red - "Traffic Mode" active
|
||||
- 🟡 Yellow - "Conditional Experimental Mode" overridden
|
||||
- "Unlimited" road UI that extends out as far as the model can see
|
||||
- Sidebar retains it's previous position between reboots/ignition cycles
|
||||
- Steering wheel icons
|
||||
- 📢 Request your own steering wheel icon in the "feature-request" channel!
|
||||
- Steering wheel in the onroad UI rotates alongside your physical steering wheel
|
||||
------
|
||||
🚙 **Vehicle Specific Additions:**
|
||||
|
||||
- Automatic/manual fingerprint selection to force a selected fingerprint
|
||||
- Custom longitudinal tunings for GM EVs and trucks for smoother gas and brake control
|
||||
- Custom longitudinal tunings for Toyota/Lexus for smoother gas and brake control
|
||||
- Disable openpilot longitudinal control to use your car's stock cruise control
|
||||
- GM Volt support
|
||||
- Honda Clarity support
|
||||
- Increased torque for the Subaru Crosstrek
|
||||
- Lock doors automatically when in the drive gear for Toyota/Lexus and unlock when in park
|
||||
- openpilot longitudinal control for GM vehicles without ACC
|
||||
- Pedal interceptor support for GM vehicles
|
||||
- "Stop and Go" hack for Toyota's without stop and go functionality
|
||||
- ZSS support for the Toyota Prius and Sienna
|
||||
------
|
||||
🚦 **Quality of Life Features:**
|
||||
|
||||
- Automatic updates for a completely "set and forget" experience
|
||||
- Camera view selection
|
||||
- Custom alerts for green lights, vehicles in blindspot, lead departing, and the current speed limit changing
|
||||
- Display the driver camera when in reverse
|
||||
- Driving statistics to show how many hours and miles you've driven on the home screen
|
||||
- Fleet Manager to easily access your driving data and screen recordings
|
||||
- Numerical temperature gauge
|
||||
- Retain tethering status between reboots
|
||||
- Screenrecorder
|
||||
- Toggle "Experimental Mode" via the "Lane Departure Alert" button, holding down the "Distance" button for 0.5+ seconds, or by double tapping the screen
|
||||
|
||||
How to Install
|
||||
openpilot vs **FrogPilot**
|
||||
------
|
||||
|
||||
Easiest way to install FrogPilot is via this URL at the installation screen:
|
||||
#### Community
|
||||
| Feature | openpilot | **FrogPilot** |
|
||||
|---------|:---------:|:---------:|
|
||||
| A Welcoming Community | ❌ | ✅ |
|
||||
| Erich / Primary Moderators / 🦇 | ✅ | ❌ |
|
||||
|
||||
#### Core Features
|
||||
| Feature | openpilot | **FrogPilot** |
|
||||
|---------|:---------:|:---------:|
|
||||
| Always On Lateral (Steering) | ❌ | ✅ |
|
||||
| Blind Spot Integration | ✅ | ✅ |
|
||||
| Conditional Experimental Mode | ❌ | ✅ |
|
||||
| Custom Themes | ❌ | ✅ |
|
||||
| Driver Monitoring | ✅ | ✅ |
|
||||
| Driving Model Selector | ❌ | ✅ |
|
||||
| Holiday Themes | ❌ | ✅ |
|
||||
| Speed Limit Support | ❌ | ✅ |
|
||||
| Weather Detection | ❌ | ✅ |
|
||||
|
||||
#### Device & Hardware
|
||||
| Feature | openpilot | **FrogPilot** |
|
||||
|---------|:---------:|:---------:|
|
||||
| Advanced Volume Controller | ❌ | ✅ |
|
||||
| Automatic Version Backups | ❌ | ✅ |
|
||||
| C3 Support | ❌ | ✅ |
|
||||
| comma Pedal Support | ❌ | ✅ |
|
||||
| High Quality Recordings | ❌ | ✅ |
|
||||
| SDSU Support | ❌ | ✅ |
|
||||
| ZSS Support | ❌ | ✅ |
|
||||
|
||||
#### Gas/Brake
|
||||
| Feature | openpilot | **FrogPilot** |
|
||||
|---------|:---------:|:---------:|
|
||||
| Adaptive Cruise Control (ACC) | ✅ | ✅ |
|
||||
| Advanced Live Tuning | ❌ | ✅ |
|
||||
| Custom Following Distances | ❌ | ✅ |
|
||||
| Faster Human-Like Acceleration | ❌ | ✅ |
|
||||
| Human-Like Speed Control in Curves | ❌ | ✅ |
|
||||
| Smoother Human-Like Braking | ❌ | ✅ |
|
||||
|
||||
#### Steering
|
||||
| Feature | openpilot | **FrogPilot** |
|
||||
|---------|:---------:|:---------:|
|
||||
| Advanced Live Tuning | ❌ | ✅ |
|
||||
| Automatic Lane Changes | ❌ | ✅ |
|
||||
| Increased Steering Torque* | ❌ | ✅ |
|
||||
| Lane Centering (LKAS) | ✅ | ✅ |
|
||||
| Lane Change Assist | ✅ | ✅ |
|
||||
|
||||
*Select vehicles only
|
||||
|
||||
And much much more!
|
||||
|
||||
🌟 Highlight Features
|
||||
------
|
||||
|
||||
### 🚗 Always On Lateral (AOL)
|
||||
|
||||
With **"Always On Lateral"**, lane-centering stays active whenever cruise control is on, even when you press the accelerator or brake. This means steering assist won't cut out during manual speed adjustments giving you continuous support through curves, traffic, or mountain roads!
|
||||
|
||||
---
|
||||
|
||||
### 🧠 Conditional Experimental Mode (CEM)
|
||||
|
||||
**["Experimental Mode"](https://blog.comma.ai/090release/#experimental-mode)** lets openpilot drive at the speed it thinks a human would to allow slowing for curves, stopping at stoplights/stop signs, and adapting to traffic. This makes it powerful in complex scenarios, but it's still, well, "experimental" and less predictable than **"Chill Mode"**. But **"Conditional Experimental Mode"** gives you the best of both worlds by automatically switching between **"Chill Mode"** for steady cruising and **"Experimental Mode"** for more advanced situations to help fully automate your driving experience!
|
||||
|
||||
**"Conditional Experimental Mode"** switches into **"Experimental Mode"** when conditions like these are met:
|
||||
- Approaching curves and turns
|
||||
- Detecting slower or stopped lead vehicles
|
||||
- Driving below a set speed
|
||||
- Predicting an upcoming stop (e.g. stoplight or stop sign)
|
||||
|
||||
Once conditions clear it returns to **"Chill Mode"** for stability and predictability.
|
||||
|
||||
**Note: Stay attentive as "Experimental Mode" is an alpha feature and mistakes are expected!**
|
||||
|
||||
---
|
||||
|
||||
### 🎭 Driving Personalities
|
||||
|
||||
With **"Driving Personalities"**, you choose how the vehicle behaves with four adjustable profiles:
|
||||
|
||||
- **Traffic:** Catered towards stop-and-go traffic by minimizing gaps and delays
|
||||
- **Aggressive:** Aimed to provide tighter following distances and quicker reactions
|
||||
- **Standard:** Useful for a balanced, all-purpose driving
|
||||
- **Relaxed:** A smoother driving experience with larger following distance gaps
|
||||
|
||||
Each profile can be fine-tuned to change the desired following distance, acceleration, and braking style letting you shape **FrogPilot**'s behavior to match your own driving preferences! Profiles can be switched instantly using the following distance button on the steering wheel, while **"Traffic Mode"** can be enabled by simply holding down the following distance button.
|
||||
|
||||
---
|
||||
|
||||
### 📏 Speed Limit Controller (SLC)
|
||||
|
||||
With **"Speed Limit Controller"**, **FrogPilot** automatically adapts to the road's posted speed using information from downloaded **["OpenStreetMap"](https://www.openstreetmap.org)** maps, online **["Mapbox"](https://www.mapbox.com)** data, and the vehicle's dashboard (if supported).
|
||||
|
||||
Offsets let you fine-tune how closely **FrogPilot** follows posted limits across different speed ranges allowing you to cruise slightly above or below for a more natural driving experience. If no speed limit is available, you can choose whether **FrogPilot** drives at the set speed, falls back to the last known speed limit, or uses **"Experimental Mode"** to estimate one with the driving model.
|
||||
|
||||
Maps can be downloaded directly in settings and updated automatically on a schedule ensuring your device always has the latest speed limits!
|
||||
|
||||
**Note: Speed limits are only as accurate as the available speed limit data. Always stay attentive and adjust your speed when necessary!**
|
||||
|
||||
---
|
||||
|
||||
### 🎨 Themes
|
||||
|
||||
With **"Themes"**, you can personalize **FrogPilot**'s driving screen to make it uniquely yours! Choose from:
|
||||
|
||||
- **Color Schemes**
|
||||
- **Icon Packs**
|
||||
- **Sound Packs**
|
||||
- **Turn Signal Animations**
|
||||
- **Steering Wheel Icons**
|
||||
|
||||
Enjoy pre-existing **FrogPilot** and seasonal holiday themes, or you can create your own with the **"Theme Maker"** and even share them with the community! For extra fun, enable features like the Mario Kart–style **"Rainbow Path"** or **"Random Events"** that add playful visual effects while you drive!
|
||||
|
||||
---
|
||||
|
||||
And lots more! From safety enhancements to personalization options, **FrogPilot** continues to evolve with features that put you in control. Check it out today for yourself!
|
||||
|
||||
---
|
||||
|
||||
🔧 Branches
|
||||
------
|
||||
| Branch | Install URL | Description | Recommended For |
|
||||
|----------------------------|---------------------------|--------------------------------------------------------|--------------------------|
|
||||
| FrogPilot | frogpilot.download | The main release branch. | Everyone |
|
||||
| FrogPilot‑Staging | staging.frogpilot.download| Beta branch with upcoming features. Expect bugs! | Early Adopters |
|
||||
| FrogPilot‑Testing | testing.frogpilot.download| Alpha branch with bleeding-edge features. Breaks often!| Advanced Testers |
|
||||
| FrogPilot‑Development| No :) | Active development branch. Don't use! | **FrogPilot** Developers|
|
||||
| MAKE‑PRS‑HERE | No :) | Workspace for pull requests. Don't use! | Contributors |
|
||||
|
||||
🧰 How to Install
|
||||
------
|
||||
|
||||
The easiest way to install **FrogPilot** is by entering this URL on the installation screen:
|
||||
|
||||
```
|
||||
frogpilot.download
|
||||
```
|
||||
|
||||
DO NOT install the "FrogPilot-Development" branch. I'm constantly breaking things on there, so unless you don't want to use openpilot, NEVER install it!
|
||||
**DO NOT** install the **FrogPilot-Development** branch. I'm constantly breaking things on there, so unless you don't want to use **FrogPilot**, **NEVER** install it!
|
||||
|
||||

|
||||

|
||||
|
||||
Bug reports / Feature Requests
|
||||
🐞 Bug Reports / Feature Requests
|
||||
------
|
||||
|
||||
If you encounter any issues or bugs while using FrogPilot, or if you have any suggestions for new features or improvements, please don't hesitate to post about it on the Discord! I'm always looking for ways to improve the fork and provide a better experience for everyone!
|
||||
If you run into bugs, issues, or have ideas for new features, please post about it on the **[FrogPilot Discord](https://discord.gg/frogpilot)**! Feedback helps improve **FrogPilot** and create a better experience for everyone!
|
||||
|
||||
To report a bug or request a new feature, make a post in the #bug-reports or #feature-requests channel respectively on the FrogPilot Discord. Please provide as much detail as possible about the issue you're experiencing or the feature you'd like to see added. Photos, videos, log files, or other relevant information are very helpful!
|
||||
To report a bug, please post it in [**#bug-reports**](https://discord.com/channels/1137853399715549214/1162100167110053888).
|
||||
To request a feature, please post it in [**#feature-requests**](https://discord.com/channels/1137853399715549214/1160318669839147259).
|
||||
|
||||
I will do my best to respond to bug reports and feature requests in a timely manner, but please understand that I may not be able to address every request immediately. Your feedback and suggestions are valuable, and I appreciate your help in making FrogPilot the best it can be!
|
||||
Please include as much detail as possible! Photos, videos, log files, or anything that can help explain the issue or idea are very helpful!
|
||||
|
||||
Discord
|
||||
------
|
||||
I'll do my best to respond promptly, but not every request can be addressed right away. Your feedback is always appreciated and helps make **FrogPilot** the best it can be!
|
||||
|
||||
[Join the FrogPilot Community Discord!](https://discord.gg/frogpilot)
|
||||
|
||||
Credits
|
||||
📋 Credits
|
||||
------
|
||||
|
||||
* [Aidenir](https://github.com/Aidenir)
|
||||
* [AlexandreSato](https://github.com/AlexandreSato)
|
||||
* [Crwusiz](https://github.com/crwusiz)
|
||||
* [DragonPilot](https://github.com/dragonpilot-community)
|
||||
* [cfranyota](https://github.com/cfranyota)
|
||||
* [cydia2020](https://github.com/cydia2020)
|
||||
* [dragonpilot-community](https://github.com/dragonpilot-community)
|
||||
* [ErichMoraga](https://github.com/ErichMoraga)
|
||||
* [Garrettpall](https://github.com/garrettpall)
|
||||
* [Mike8643](https://github.com/mike8643)
|
||||
* [Neokii](https://github.com/Neokii)
|
||||
* [garrettpall](https://github.com/garrettpall)
|
||||
* [jakethesnake420](https://github.com/jakethesnake420)
|
||||
* [jyoung8607](https://github.com/jyoung8607)
|
||||
* [mike8643](https://github.com/mike8643)
|
||||
* [neokii](https://github.com/neokii)
|
||||
* [OPGM](https://github.com/opgm)
|
||||
* [OPKR](https://github.com/openpilotkr)
|
||||
* [Pfeiferj](https://github.com/pfeiferj)
|
||||
* [ServerDummy](https://github.com/ServerDummy)
|
||||
* [Twilsonco](https://github.com/twilsonco)
|
||||
* [pfeiferj](https://github.com/pfeiferj)
|
||||
* [realfast](https://github.com/realfast)
|
||||
* [syncword](https://github.com/syncword)
|
||||
* [twilsonco](https://github.com/twilsonco)
|
||||
|
||||
Licensing
|
||||
Star History
|
||||
------
|
||||
|
||||
openpilot is released under the MIT license. Some parts of the software are released under other licenses as specified.
|
||||
|
||||
Any user of this software shall indemnify and hold harmless Comma.ai, Inc. and its directors, officers, employees, agents, stockholders, affiliates, subcontractors and customers from and against all allegations, claims, actions, suits, demands, damages, liabilities, obligations, losses, settlements, judgments, costs and expenses (including without limitation attorneys’ fees and costs) which arise out of, relate to or result from any use of this software by user.
|
||||
|
||||
**THIS IS ALPHA QUALITY SOFTWARE FOR RESEARCH PURPOSES ONLY. THIS IS NOT A PRODUCT.
|
||||
YOU ARE RESPONSIBLE FOR COMPLYING WITH LOCAL LAWS AND REGULATIONS.
|
||||
NO WARRANTY EXPRESSED OR IMPLIED.**
|
||||
|
||||
---
|
||||
|
||||
<img src="https://d1qb2nb5cznatu.cloudfront.net/startups/i/1061157-bc7e9bf3b246ece7322e6ffe653f6af8-medium_jpg.jpg?buster=1458363130" width="75"></img> <img src="https://cdn-images-1.medium.com/max/1600/1*C87EjxGeMPrkTuVRVWVg4w.png" width="225"></img>
|
||||
|
||||
[](https://deepwiki.com/FrogAi/FrogPilot)
|
||||

|
||||
[](https://codecov.io/gh/commaai/openpilot)
|
||||
[](https://www.star-history.com/#FrogAi/FrogPilot&Date)
|
||||
|
||||
@@ -118,37 +118,6 @@ struct CarEvent @0x9b1657f34caf3ad3 {
|
||||
paramsdPermanentError @119;
|
||||
actuatorsApiUnavailable @120;
|
||||
|
||||
# FrogPilot Events
|
||||
accel30 @122;
|
||||
accel35 @123;
|
||||
accel40 @124;
|
||||
blockUser @125;
|
||||
customStartupAlert @126;
|
||||
dejaVuCurve @127;
|
||||
firefoxSteerSaturated @128;
|
||||
forcingStop @129;
|
||||
goatSteerSaturated @130;
|
||||
greenLight @131;
|
||||
hal9000 @132;
|
||||
holidayActive @133;
|
||||
laneChangeBlockedLoud @134;
|
||||
leadDeparting @135;
|
||||
noLaneAvailable @136;
|
||||
openpilotCrashed @137;
|
||||
openpilotCrashedRandomEvent @138;
|
||||
pedalInterceptorNoBrake @139;
|
||||
speedLimitChanged @140;
|
||||
thisIsFineSteerSaturated @141;
|
||||
toBeContinued @142;
|
||||
torqueNNLoad @143;
|
||||
trafficModeActive @144;
|
||||
trafficModeInactive @145;
|
||||
turningLeft @146;
|
||||
turningRight @147;
|
||||
vCruise69 @148;
|
||||
yourFrogTriedToKillMe @149;
|
||||
youveGotMail @150;
|
||||
|
||||
radarCanErrorDEPRECATED @15;
|
||||
communityFeatureDisallowedDEPRECATED @62;
|
||||
radarCommIssueDEPRECATED @67;
|
||||
@@ -444,22 +413,6 @@ struct CarControl {
|
||||
prompt @6;
|
||||
promptRepeat @7;
|
||||
promptDistracted @8;
|
||||
|
||||
# FrogPilot sounds
|
||||
angry @9;
|
||||
continued @10;
|
||||
dejaVu @11;
|
||||
doc @12;
|
||||
fart @13;
|
||||
firefox @14;
|
||||
goat @15;
|
||||
hal9000 @16;
|
||||
mail @17;
|
||||
nessie @18;
|
||||
noice @19;
|
||||
startup @20;
|
||||
thisIsFine @21;
|
||||
uwu @22;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -10,19 +10,113 @@ using Car = import "car.capnp";
|
||||
# cereal, so use these if you want custom events in your fork.
|
||||
|
||||
# you can rename the struct, but don't change the identifier
|
||||
struct FrogPilotCarParams @0x81c2f05a394cf4af {
|
||||
fpFlags @0 :UInt32;
|
||||
isHDA2 @1 :Bool;
|
||||
openpilotLongitudinalControlDisabled @2 :Bool;
|
||||
}
|
||||
struct FrogPilotCarControl {
|
||||
hudControl @0 :HUDControl;
|
||||
|
||||
struct FrogPilotCarState @0xaedffd8f31e7b55d {
|
||||
struct ButtonEvent {
|
||||
enum Type {
|
||||
lkas @0;
|
||||
struct HUDControl {
|
||||
audibleAlert @0 :AudibleAlert;
|
||||
|
||||
enum AudibleAlert {
|
||||
none @0;
|
||||
|
||||
engage @1;
|
||||
disengage @2;
|
||||
refuse @3;
|
||||
|
||||
warningSoft @4;
|
||||
warningImmediate @5;
|
||||
|
||||
prompt @6;
|
||||
promptRepeat @7;
|
||||
promptDistracted @8;
|
||||
|
||||
# Random Events
|
||||
angry @9;
|
||||
continued @10;
|
||||
dejaVu @11;
|
||||
doc @12;
|
||||
fart @13;
|
||||
firefox @14;
|
||||
goat @15;
|
||||
hal9000 @16;
|
||||
mail @17;
|
||||
nessie @18;
|
||||
noice @19;
|
||||
startup @20;
|
||||
thisIsFine @21;
|
||||
uwu @22;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct FrogPilotCarEvent @0x81c2f05a394cf4af {
|
||||
name @0 :EventName;
|
||||
|
||||
enable @1 :Bool;
|
||||
noEntry @2 :Bool;
|
||||
warning @3 :Bool;
|
||||
userDisable @4 :Bool;
|
||||
softDisable @5 :Bool;
|
||||
immediateDisable @6 :Bool;
|
||||
preEnable @7 :Bool;
|
||||
permanent @8 :Bool;
|
||||
overrideLateral @10 :Bool;
|
||||
overrideLongitudinal @9 :Bool;
|
||||
|
||||
enum EventName @0xaedffd8f31e7b55d {
|
||||
blockUser @0;
|
||||
customStartupAlert @1;
|
||||
forcingStop @2;
|
||||
goatSteerSaturated @3;
|
||||
greenLight @4;
|
||||
holidayActive @5;
|
||||
laneChangeBlockedLoud @6;
|
||||
leadDeparting @7;
|
||||
noLaneAvailable @8;
|
||||
openpilotCrashed @9;
|
||||
pedalInterceptorNoBrake @10;
|
||||
speedLimitChanged @11;
|
||||
torqueNNLoad @12;
|
||||
trafficModeActive @13;
|
||||
trafficModeInactive @14;
|
||||
turningLeft @15;
|
||||
turningRight @16;
|
||||
|
||||
# Random Events
|
||||
accel30 @17;
|
||||
accel35 @18;
|
||||
accel40 @19;
|
||||
dejaVuCurve @20;
|
||||
firefoxSteerSaturated @21;
|
||||
hal9000 @22;
|
||||
openpilotCrashedRandomEvent @23;
|
||||
thisIsFineSteerSaturated @24;
|
||||
toBeContinued @25;
|
||||
vCruise69 @26;
|
||||
yourFrogTriedToKillMe @27;
|
||||
youveGotMail @28;
|
||||
}
|
||||
}
|
||||
|
||||
struct FrogPilotCarParams @0xf35cc4560bbf6ec2 {
|
||||
canUsePedal @0 :Bool;
|
||||
canUseSDSU @1 :Bool;
|
||||
fpFlags @2 :UInt32;
|
||||
isHDA2 @3 :Bool;
|
||||
openpilotLongitudinalControlDisabled @4 :Bool;
|
||||
safetyConfigs @5 :List(SafetyConfig);
|
||||
|
||||
lateralTuning :union {
|
||||
pid @6 :Car.CarParams.LateralPIDTuning;
|
||||
torque @7 :Car.CarParams.LateralTorqueTuning;
|
||||
}
|
||||
|
||||
struct SafetyConfig {
|
||||
safetyParam @0 :UInt16;
|
||||
}
|
||||
}
|
||||
|
||||
struct FrogPilotCarState @0xda96579883444c35 {
|
||||
accelPressed @0 :Bool;
|
||||
alwaysOnLateralAllowed @1 :Bool;
|
||||
alwaysOnLateralEnabled @2 :Bool;
|
||||
@@ -38,67 +132,118 @@ struct FrogPilotCarState @0xaedffd8f31e7b55d {
|
||||
pauseLongitudinal @12 :Bool;
|
||||
sportGear @13 :Bool;
|
||||
trafficModeEnabled @14 :Bool;
|
||||
|
||||
struct ButtonEvent {
|
||||
enum Type {
|
||||
lkas @0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct FrogPilotDeviceState @0xf35cc4560bbf6ec2 {
|
||||
struct FrogPilotControlsState @0x80ae746ee2596b11 {
|
||||
alertStatus @0 :AlertStatus;
|
||||
alertText1 @1 :Text;
|
||||
alertText2 @2 :Text;
|
||||
alertSize @3 :AlertSize;
|
||||
alertBlinkingRate @4 :Float32;
|
||||
alertType @5 :Text;
|
||||
alertSound @6 :Car.CarControl.HUDControl.AudibleAlert;
|
||||
|
||||
enum AlertSize {
|
||||
none @0; # don't display the alert
|
||||
small @1; # small box
|
||||
mid @2; # mid screen
|
||||
full @3; # full screen
|
||||
}
|
||||
|
||||
enum AlertStatus {
|
||||
normal @0; # low priority alert for user's convenience
|
||||
userPrompt @1; # mid priority alert that might require user intervention
|
||||
critical @2; # high priority alert that needs immediate user intervention
|
||||
frogpilot @3; # FrogPilot startup alert
|
||||
}
|
||||
}
|
||||
|
||||
struct FrogPilotDeviceState @0xa5cd762cd951a455 {
|
||||
freeSpace @0 :Int16;
|
||||
usedSpace @1 :Int16;
|
||||
}
|
||||
|
||||
struct FrogPilotNavigation @0xda96579883444c35 {
|
||||
struct FrogPilotModelDataV2 @0xf98d843bfd7004a3 {
|
||||
turnDirection @0 :TurnDirection;
|
||||
|
||||
enum TurnDirection {
|
||||
none @0;
|
||||
turnLeft @1;
|
||||
turnRight @2;
|
||||
}
|
||||
}
|
||||
|
||||
struct FrogPilotNavigation @0xf416ec09499d9d19 {
|
||||
approachingIntersection @0 :Bool;
|
||||
approachingTurn @1 :Bool;
|
||||
navigationSpeedLimit @2 :Float32;
|
||||
}
|
||||
|
||||
struct FrogPilotPlan @0x80ae746ee2596b11 {
|
||||
struct FrogPilotPlan @0xa1680744031fdb2d {
|
||||
accelerationJerk @0 :Float32;
|
||||
accelerationJerkStock @1 :Float32;
|
||||
dangerJerk @2 :Float32;
|
||||
desiredFollowDistance @3 :Int64;
|
||||
experimentalMode @4 :Bool;
|
||||
forcingStop @5 :Bool;
|
||||
forcingStopLength @6 :Float32;
|
||||
frogpilotEvents @7 :List(Car.CarEvent);
|
||||
lateralCheck @8 :Bool;
|
||||
laneWidthLeft @9 :Float32;
|
||||
laneWidthRight @10 :Float32;
|
||||
maxAcceleration @11 :Float32;
|
||||
minAcceleration @12 :Float32;
|
||||
mtscSpeed @13 :Float32;
|
||||
redLight @14 :Bool;
|
||||
roadCurvature @15 :Float32;
|
||||
slcMapSpeedLimit @16 :Float32;
|
||||
slcMapboxSpeedLimit @17 :Float32;
|
||||
slcNextSpeedLimit @18 :Float32;
|
||||
slcOverriddenSpeed @19 :Float32;
|
||||
slcSpeedLimit @20 :Float32;
|
||||
slcSpeedLimitOffset @21 :Float32;
|
||||
slcSpeedLimitSource @22 :Text;
|
||||
speedJerk @23 :Float32;
|
||||
speedJerkStock @24 :Float32;
|
||||
speedLimitChanged @25 :Bool;
|
||||
tFollow @26 :Float32;
|
||||
themeUpdated @27 :Bool;
|
||||
togglesUpdated @28 :Bool;
|
||||
trackingLead @29 :Bool;
|
||||
unconfirmedSlcSpeedLimit @30 :Float32;
|
||||
vCruise @31 :Float32;
|
||||
vtscControllingCurve @32 :Bool;
|
||||
vtscSpeed @33 :Float32;
|
||||
cscControllingSpeed @2 :Bool;
|
||||
cscSpeed @3 :Float32;
|
||||
cscTraining @4 :Bool;
|
||||
dangerJerk @5 :Float32;
|
||||
desiredFollowDistance @6 :Int64;
|
||||
experimentalMode @7 :Bool;
|
||||
forcingStop @8 :Bool;
|
||||
forcingStopLength @9 :Float32;
|
||||
frogpilotEvents @10 :List(FrogPilotCarEvent);
|
||||
increasedStoppedDistance @11 :Float32;
|
||||
lateralCheck @12 :Bool;
|
||||
laneWidthLeft @13 :Float32;
|
||||
laneWidthRight @14 :Float32;
|
||||
maxAcceleration @15 :Float32;
|
||||
minAcceleration @16 :Float32;
|
||||
redLight @17 :Bool;
|
||||
roadCurvature @18 :Float32;
|
||||
slcMapSpeedLimit @19 :Float32;
|
||||
slcMapboxSpeedLimit @20 :Float32;
|
||||
slcNextSpeedLimit @21 :Float32;
|
||||
slcOverriddenSpeed @22 :Float32;
|
||||
slcSpeedLimit @23 :Float32;
|
||||
slcSpeedLimitOffset @24 :Float32;
|
||||
slcSpeedLimitSource @25 :Text;
|
||||
speedJerk @26 :Float32;
|
||||
speedJerkStock @27 :Float32;
|
||||
speedLimitChanged @28 :Bool;
|
||||
tFollow @29 :Float32;
|
||||
themeUpdated @30 :Bool;
|
||||
togglesUpdated @31 :Bool;
|
||||
trackingLead @32 :Bool;
|
||||
unconfirmedSlcSpeedLimit @33 :Float32;
|
||||
vCruise @34 :Float32;
|
||||
}
|
||||
|
||||
struct CustomReserved5 @0xa5cd762cd951a455 {
|
||||
}
|
||||
struct FrogPilotRadarState @0xcb9fd56c7057593a {
|
||||
leadLeft @0 :LeadData;
|
||||
leadRight @1 :LeadData;
|
||||
|
||||
struct CustomReserved6 @0xf98d843bfd7004a3 {
|
||||
}
|
||||
struct LeadData {
|
||||
dRel @0 :Float32;
|
||||
yRel @1 :Float32;
|
||||
vRel @2 :Float32;
|
||||
aRel @3 :Float32;
|
||||
vLead @4 :Float32;
|
||||
dPath @6 :Float32;
|
||||
vLat @7 :Float32;
|
||||
vLeadK @8 :Float32;
|
||||
aLeadK @9 :Float32;
|
||||
fcw @10 :Bool;
|
||||
status @11 :Bool;
|
||||
aLeadTau @12 :Float32;
|
||||
modelProb @13 :Float32;
|
||||
radar @14 :Bool;
|
||||
radarTrackId @15 :Int32 = -1;
|
||||
|
||||
struct CustomReserved7 @0xb86e6369214c01c8 {
|
||||
}
|
||||
|
||||
struct CustomReserved8 @0xf416ec09499d9d19 {
|
||||
}
|
||||
|
||||
struct CustomReserved9 @0xa1680744031fdb2d {
|
||||
aLeadDEPRECATED @5 :Float32;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -335,12 +335,6 @@ enum LaneChangeDirection {
|
||||
right @2;
|
||||
}
|
||||
|
||||
enum TurnDirection {
|
||||
none @0;
|
||||
turnLeft @1;
|
||||
turnRight @2;
|
||||
}
|
||||
|
||||
struct CanData {
|
||||
address @0 :UInt32;
|
||||
busTime @1 :UInt16;
|
||||
@@ -618,8 +612,6 @@ struct RadarState @0x9a185389d6fdd05f {
|
||||
|
||||
leadOne @3 :LeadData;
|
||||
leadTwo @4 :LeadData;
|
||||
leadLeft @13 :LeadData;
|
||||
leadRight @14 :LeadData;
|
||||
cumLagMs @5 :Float32;
|
||||
|
||||
struct LeadData {
|
||||
@@ -638,7 +630,6 @@ struct RadarState @0x9a185389d6fdd05f {
|
||||
modelProb @13 :Float32;
|
||||
radar @14 :Bool;
|
||||
radarTrackId @15 :Int32 = -1;
|
||||
farLead @16 :Bool;
|
||||
|
||||
aLeadDEPRECATED @5 :Float32;
|
||||
}
|
||||
@@ -754,7 +745,6 @@ struct ControlsState @0x97ff69c53601abf1 {
|
||||
normal @0; # low priority alert for user's convenience
|
||||
userPrompt @1; # mid priority alert that might require user intervention
|
||||
critical @2; # high priority alert that needs immediate user intervention
|
||||
frogpilot @3; # FrogPilot startup alert
|
||||
}
|
||||
|
||||
enum AlertSize {
|
||||
@@ -805,7 +795,6 @@ struct ControlsState @0x97ff69c53601abf1 {
|
||||
saturated @7 :Bool;
|
||||
actualLateralAccel @9 :Float32;
|
||||
desiredLateralAccel @10 :Float32;
|
||||
nnLog @11 :List(Float32);
|
||||
}
|
||||
|
||||
struct LateralLQRState {
|
||||
@@ -908,7 +897,6 @@ struct DrivingModelData {
|
||||
struct MetaData {
|
||||
laneChangeState @0 :LaneChangeState;
|
||||
laneChangeDirection @1 :LaneChangeDirection;
|
||||
turnDirection @2 :TurnDirection;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1002,7 +990,6 @@ struct ModelDataV2 {
|
||||
hardBrakePredicted @7 :Bool;
|
||||
laneChangeState @8 :LaneChangeState;
|
||||
laneChangeDirection @9 :LaneChangeDirection;
|
||||
turnDirection @10 :TurnDirection;
|
||||
|
||||
|
||||
# deprecated
|
||||
@@ -2418,16 +2405,16 @@ struct Event {
|
||||
customReservedRawData2 @126 :Data;
|
||||
|
||||
# *********** Custom: reserved for forks ***********
|
||||
frogpilotCarParams @107 :Custom.FrogPilotCarParams;
|
||||
frogpilotCarState @108 :Custom.FrogPilotCarState;
|
||||
frogpilotDeviceState @109 :Custom.FrogPilotDeviceState;
|
||||
frogpilotNavigation @110 :Custom.FrogPilotNavigation;
|
||||
frogpilotPlan @111 :Custom.FrogPilotPlan;
|
||||
customReserved5 @112 :Custom.CustomReserved5;
|
||||
customReserved6 @113 :Custom.CustomReserved6;
|
||||
customReserved7 @114 :Custom.CustomReserved7;
|
||||
customReserved8 @115 :Custom.CustomReserved8;
|
||||
customReserved9 @116 :Custom.CustomReserved9;
|
||||
frogpilotCarControl @107 :Custom.FrogPilotCarControl;
|
||||
frogpilotCarParams @108 :Custom.FrogPilotCarParams;
|
||||
frogpilotCarState @109 :Custom.FrogPilotCarState;
|
||||
frogpilotControlsState @110 :Custom.FrogPilotControlsState;
|
||||
frogpilotDeviceState @111 :Custom.FrogPilotDeviceState;
|
||||
frogpilotModelV2 @112 :Custom.FrogPilotModelDataV2;
|
||||
frogpilotNavigation @113 :Custom.FrogPilotNavigation;
|
||||
frogpilotOnroadEvents @114: List(Custom.FrogPilotCarEvent);
|
||||
frogpilotPlan @115 :Custom.FrogPilotPlan;
|
||||
frogpilotRadarState @116 :Custom.FrogPilotRadarState;
|
||||
|
||||
# *********** legacy + deprecated ***********
|
||||
model @9 :Legacy.ModelData; # TODO: rename modelV2 and mark this as deprecated
|
||||
|
||||
@@ -94,11 +94,16 @@ _services: dict[str, tuple] = {
|
||||
"customReservedRawData2": (True, 0.),
|
||||
|
||||
# FrogPilot
|
||||
"frogpilotCarControl": (True, 100., 10),
|
||||
"frogpilotCarParams": (True, 0.02, 1),
|
||||
"frogpilotCarState": (True, 100., 10),
|
||||
"frogpilotControlsState": (True, 100., 10),
|
||||
"frogpilotDeviceState": (True, 2., 1),
|
||||
"frogpilotModelV2": (True, 20., 40),
|
||||
"frogpilotNavigation": (True, 1., 10),
|
||||
"frogpilotOnroadEvents": (True, 1., 1),
|
||||
"frogpilotPlan": (True, 20., 5),
|
||||
"frogpilotRadarState": (True, 20., 5),
|
||||
}
|
||||
SERVICE_LIST = {name: Service(*vals) for
|
||||
idx, (name, vals) in enumerate(_services.items())}
|
||||
|
||||
@@ -14,6 +14,8 @@ class Conversions:
|
||||
# Distance
|
||||
METER_TO_FOOT = 3.28084
|
||||
FOOT_TO_METER = 1. / METER_TO_FOOT
|
||||
METER_TO_MILE = METER_TO_FOOT / 5280
|
||||
MILE_TO_METER = 1. / METER_TO_MILE
|
||||
CM_TO_INCH = 1. / 2.54
|
||||
INCH_TO_CM = 1. / CM_TO_INCH
|
||||
|
||||
|
||||
@@ -246,6 +246,8 @@ std::unordered_map<std::string, uint32_t> keys = {
|
||||
{"BlindSpotMetrics", PERSISTENT},
|
||||
{"BlindSpotPath", PERSISTENT},
|
||||
{"BorderMetrics", PERSISTENT},
|
||||
{"CalibratedLateralAcceleration", PERSISTENT},
|
||||
{"CalibrationProgress", PERSISTENT},
|
||||
{"CameraView", PERSISTENT},
|
||||
{"CancelModelDownload", CLEAR_ON_MANAGER_START},
|
||||
{"CancelThemeDownload", CLEAR_ON_MANAGER_START},
|
||||
@@ -271,8 +273,8 @@ std::unordered_map<std::string, uint32_t> keys = {
|
||||
{"ColorToDownload", CLEAR_ON_MANAGER_START},
|
||||
{"Compass", PERSISTENT},
|
||||
{"ConditionalExperimental", PERSISTENT},
|
||||
{"CurveSensitivity", PERSISTENT},
|
||||
{"CurveSpeedControl", PERSISTENT},
|
||||
{"CurvatureData", PERSISTENT | DONT_LOG},
|
||||
{"CurveSpeedController", PERSISTENT},
|
||||
{"CustomAlerts", PERSISTENT},
|
||||
{"CustomColors", PERSISTENT},
|
||||
{"CustomCruise", PERSISTENT},
|
||||
@@ -327,8 +329,8 @@ std::unordered_map<std::string, uint32_t> keys = {
|
||||
{"ForceMPHDashboard", PERSISTENT},
|
||||
{"ForceOffroad", CLEAR_ON_MANAGER_START},
|
||||
{"ForceOnroad", CLEAR_ON_MANAGER_START},
|
||||
{"ForceStandstill", PERSISTENT},
|
||||
{"ForceStops", PERSISTENT},
|
||||
{"ForceTorqueController", PERSISTENT},
|
||||
{"FPSCounter", PERSISTENT},
|
||||
{"FrogPilotCarParams", CLEAR_ON_MANAGER_START | CLEAR_ON_ONROAD_TRANSITION},
|
||||
{"FrogPilotCarParamsPersistent", PERSISTENT},
|
||||
@@ -347,6 +349,7 @@ std::unordered_map<std::string, uint32_t> keys = {
|
||||
{"GreenLightAlert", PERSISTENT},
|
||||
{"HideAlerts", PERSISTENT},
|
||||
{"HideLeadMarker", PERSISTENT},
|
||||
{"HideMap", PERSISTENT},
|
||||
{"HideMapIcon", PERSISTENT},
|
||||
{"HideMaxSpeed", PERSISTENT},
|
||||
{"HideSpeed", PERSISTENT},
|
||||
@@ -361,7 +364,7 @@ std::unordered_map<std::string, uint32_t> keys = {
|
||||
{"IssueReported", CLEAR_ON_MANAGER_START},
|
||||
{"KonikDongleId", PERSISTENT},
|
||||
{"KonikMinutes", PERSISTENT},
|
||||
{"LaneChangeCustomizations", PERSISTENT},
|
||||
{"LaneChanges", PERSISTENT},
|
||||
{"LaneChangeTime", PERSISTENT},
|
||||
{"LaneDetectionWidth", PERSISTENT},
|
||||
{"LaneLinesWidth", PERSISTENT},
|
||||
@@ -391,8 +394,6 @@ std::unordered_map<std::string, uint32_t> keys = {
|
||||
{"MapsSelected", PERSISTENT},
|
||||
{"MapSpeedLimit", CLEAR_ON_MANAGER_START},
|
||||
{"MapStyle", PERSISTENT},
|
||||
{"MapTargetVelocities", CLEAR_ON_MANAGER_START},
|
||||
{"MapTurnControl", PERSISTENT},
|
||||
{"MaxDesiredAcceleration", PERSISTENT},
|
||||
{"MinimumBackupSize", PERSISTENT},
|
||||
{"MinimumLaneChangeSpeed", PERSISTENT},
|
||||
@@ -403,7 +404,6 @@ std::unordered_map<std::string, uint32_t> keys = {
|
||||
{"ModelToDownload", CLEAR_ON_MANAGER_START},
|
||||
{"ModelUI", PERSISTENT},
|
||||
{"ModelVersions", PERSISTENT},
|
||||
{"MTSCCurvatureCheck", PERSISTENT},
|
||||
{"NavigationUI", PERSISTENT},
|
||||
{"NextMapSpeedLimit", CLEAR_ON_MANAGER_START},
|
||||
{"NewLongAPI", PERSISTENT},
|
||||
@@ -476,6 +476,7 @@ std::unordered_map<std::string, uint32_t> keys = {
|
||||
{"ShowGPU", PERSISTENT},
|
||||
{"ShowIP", PERSISTENT},
|
||||
{"ShowMemoryUsage", PERSISTENT},
|
||||
{"ShownToggleDescriptions", PERSISTENT},
|
||||
{"ShowSLCOffset", PERSISTENT},
|
||||
{"ShowSpeedLimits", PERSISTENT},
|
||||
{"ShowSteering", PERSISTENT},
|
||||
@@ -540,6 +541,9 @@ std::unordered_map<std::string, uint32_t> keys = {
|
||||
{"TestAlert", CLEAR_ON_MANAGER_START},
|
||||
{"TetheringEnabled", PERSISTENT},
|
||||
{"ThemeDownloadProgress", CLEAR_ON_MANAGER_START},
|
||||
{"ThemesDownloaded", PERSISTENT},
|
||||
{"TinygradUpdateAvailable", PERSISTENT},
|
||||
{"TogglesUpdated", PERSISTENT},
|
||||
{"ToyotaDoors", PERSISTENT},
|
||||
{"TrafficFollow", PERSISTENT},
|
||||
{"TrafficJerkAcceleration", PERSISTENT},
|
||||
@@ -550,14 +554,15 @@ std::unordered_map<std::string, uint32_t> keys = {
|
||||
{"TrafficPersonalityProfile", PERSISTENT},
|
||||
{"TuningLevel", PERSISTENT},
|
||||
{"TuningLevelConfirmed", PERSISTENT},
|
||||
{"TurnAggressiveness", PERSISTENT},
|
||||
{"TurnDesires", PERSISTENT},
|
||||
{"UnlimitedLength", PERSISTENT},
|
||||
{"UnlockDoors", PERSISTENT},
|
||||
{"Updated", PERSISTENT},
|
||||
{"UpdateSpeedLimits", CLEAR_ON_MANAGER_START},
|
||||
{"UpdateSpeedLimitsStatus", CLEAR_ON_MANAGER_START},
|
||||
{"UpdateTinygrad", CLEAR_ON_MANAGER_START},
|
||||
{"UpdateWheelImage", CLEAR_ON_MANAGER_START},
|
||||
{"UseActiveTheme", CLEAR_ON_MANAGER_START},
|
||||
{"UseKonikServer", PERSISTENT},
|
||||
{"UseSI", PERSISTENT},
|
||||
{"UseVienna", PERSISTENT},
|
||||
@@ -566,7 +571,6 @@ std::unordered_map<std::string, uint32_t> keys = {
|
||||
{"VEgoStopping", PERSISTENT},
|
||||
{"VEgoStoppingStock", PERSISTENT},
|
||||
{"VeryLongDistanceButtonControl", PERSISTENT},
|
||||
{"VisionTurnControl", PERSISTENT},
|
||||
{"VoltSNG", PERSISTENT},
|
||||
{"WarningImmediateVolume", PERSISTENT},
|
||||
{"WarningSoftVolume", PERSISTENT},
|
||||
|
||||
@@ -30,7 +30,7 @@ def check_github_rate_limit(session):
|
||||
print(f"Error checking GitHub rate limit: {exception}")
|
||||
return False
|
||||
|
||||
def download_file(cancel_param, destination, progress_param, url, download_param, session, files=1, file_number=1):
|
||||
def download_file(cancel_param, destination, progress_param, url, download_param, session, offset_bytes=0, total_bytes=0):
|
||||
try:
|
||||
destination.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
@@ -57,8 +57,10 @@ def download_file(cancel_param, destination, progress_param, url, download_param
|
||||
temp_file.write(chunk)
|
||||
downloaded_size += len(chunk)
|
||||
|
||||
file_progress = downloaded_size / total_size
|
||||
overall_progress = ((file_number - 1) + file_progress) / files * 100
|
||||
if total_bytes:
|
||||
overall_progress = (offset_bytes + downloaded_size) / total_bytes * 100
|
||||
else:
|
||||
overall_progress = downloaded_size / total_size * 100
|
||||
|
||||
if overall_progress != 100:
|
||||
params_memory.put(progress_param, f"{overall_progress:.0f}%")
|
||||
@@ -98,10 +100,10 @@ def handle_error(destination, error_message, error, download_param, progress_par
|
||||
|
||||
def handle_request_error(error, destination, download_param, progress_param):
|
||||
error_map = {
|
||||
requests.ConnectionError: "Connection dropped",
|
||||
requests.HTTPError: lambda error: f"Server error ({error.response.status_code})" if error.response else "Server error",
|
||||
requests.RequestException: "Network request error. Check connection",
|
||||
requests.Timeout: "Download timed out"
|
||||
requests.exceptions.ConnectionError: "Connection dropped",
|
||||
requests.exceptions.HTTPError: lambda error: f"Server error ({error.response.status_code})" if error and getattr(error, "response", None) else "Server error",
|
||||
requests.exceptions.RequestException: "Network request error. Check connection",
|
||||
requests.exceptions.Timeout: "Download timed out",
|
||||
}
|
||||
|
||||
error_message = error_map.get(type(error), "Unexpected error")
|
||||
|
||||
@@ -7,24 +7,30 @@ import time
|
||||
import urllib.parse
|
||||
|
||||
from pathlib import Path
|
||||
from urllib.parse import quote_plus
|
||||
|
||||
from openpilot.common.basedir import BASEDIR
|
||||
from openpilot.frogpilot.assets.download_functions import GITLAB_URL, download_file, get_repository_url, handle_error, handle_request_error, verify_download
|
||||
from openpilot.frogpilot.common.frogpilot_utilities import delete_file
|
||||
from openpilot.frogpilot.common.frogpilot_variables import DEFAULT_CLASSIC_MODEL, DEFAULT_MODEL, DEFAULT_TINYGRAD_MODEL, MODELS_PATH, RESOURCES_REPO, TINYGRAD_FILES, \
|
||||
params, params_default, params_memory
|
||||
from openpilot.frogpilot.assets.download_functions import GITLAB_URL, download_file, get_remote_file_size, get_repository_url, handle_error, handle_request_error, verify_download
|
||||
from openpilot.frogpilot.common.frogpilot_utilities import delete_file, extract_tar, load_json_file, update_json_file
|
||||
from openpilot.frogpilot.common.frogpilot_variables import DEFAULT_MODEL, MODELS_PATH, RESOURCES_REPO, TINYGRAD_FILES, params, params_default, params_memory, update_frogpilot_toggles
|
||||
|
||||
VERSION = "v15"
|
||||
VERSION = "v16"
|
||||
VERSION_PATH = MODELS_PATH / "model_version"
|
||||
|
||||
CANCEL_DOWNLOAD_PARAM = "CancelModelDownload"
|
||||
DOWNLOAD_PROGRESS_PARAM = "ModelDownloadProgress"
|
||||
MODEL_DOWNLOAD_PARAM = "ModelToDownload"
|
||||
MODEL_DOWNLOAD_ALL_PARAM = "DownloadAllModels"
|
||||
UPDATE_TINYGRAD_PARAM = "UpdateTinygrad"
|
||||
|
||||
DEFAULT_TINYGRAD_SIZE = 87746736
|
||||
TAR_FILE_NAME = f"Tinygrad_{VERSION}.tar.gz"
|
||||
|
||||
TINYGRAD_MODELD_PATH = Path(BASEDIR) / "frogpilot/tinygrad_modeld"
|
||||
TINYGRAD_REPO_PATH = Path(BASEDIR) / "tinygrad_repo"
|
||||
|
||||
class ModelManager:
|
||||
def __init__(self):
|
||||
def __init__(self, boot_run=False):
|
||||
self.downloading_model = False
|
||||
|
||||
self.available_models = (params.get("AvailableModels", encoding="utf-8") or "").split(",")
|
||||
@@ -32,29 +38,34 @@ class ModelManager:
|
||||
self.model_versions = (params.get("ModelVersions", encoding="utf-8") or "").split(",")
|
||||
|
||||
self.model_sizes_path = MODELS_PATH / "model_sizes.json"
|
||||
self.tinygrad_sizes_path = MODELS_PATH / "tinygrad_sizes.json"
|
||||
|
||||
self.model_sizes = load_json_file(self.model_sizes_path)
|
||||
self.tinygrad_sizes = load_json_file(self.tinygrad_sizes_path)
|
||||
|
||||
self.session = requests.Session()
|
||||
self.session.headers.update({"Accept-Language": "en"})
|
||||
self.session.headers.update({"User-Agent": "frogpilot-model-downloader/1.0 (https://github.com/FrogAi/FrogPilot)"})
|
||||
|
||||
def check_models(self, boot_run, repo_url):
|
||||
downloaded_models = [path for path in MODELS_PATH.iterdir() if path.is_file()]
|
||||
if boot_run:
|
||||
self.copy_default_model()
|
||||
self.validate_models()
|
||||
|
||||
def check_models(self, boot_run, repo_url):
|
||||
downloaded_models = [
|
||||
model for model in MODELS_PATH.iterdir()
|
||||
if (MODELS_PATH / f"{model}.thneed").is_file() or all((MODELS_PATH / f"{model}_{filename}").is_file() for filename, _ in TINYGRAD_FILES)
|
||||
]
|
||||
for model_file in downloaded_models:
|
||||
if model_file.name not in {self.model_sizes_path.name} and not any(model in model_file.name for model in set(self.available_models)):
|
||||
if not any(model in model_file.name for model in set(self.available_models)):
|
||||
print(f"Removing outdated model: {model_file}")
|
||||
delete_file(model_file)
|
||||
|
||||
for onnx_file in MODELS_PATH.glob("*.onnx"):
|
||||
if onnx_file.is_file():
|
||||
print(f"Deleting .onnx file: {onnx_file}")
|
||||
delete_file(onnx_file)
|
||||
|
||||
for tmp_file in MODELS_PATH.glob("tmp*"):
|
||||
if tmp_file.is_file():
|
||||
delete_file(tmp_file)
|
||||
|
||||
if params.get("Model", encoding="utf-8") not in self.available_models:
|
||||
if params.get("Model", encoding="utf-8").removesuffix("_default") not in self.available_models:
|
||||
params.put("Model", params_default.get("Model", encoding="utf-8"))
|
||||
|
||||
if not (not boot_run and params.get_bool("AutomaticallyDownloadModels")):
|
||||
@@ -65,46 +76,80 @@ class ModelManager:
|
||||
print("No model size data available. Skipping model checks...")
|
||||
return
|
||||
|
||||
local_model_sizes = self.load_model_sizes()
|
||||
|
||||
needs_download = False
|
||||
for model, version in zip(self.available_models, self.model_versions):
|
||||
if version in {"v1", "v2", "v3", "v4", "v5", "v6"}:
|
||||
need_to_update_models = False
|
||||
for model in self.available_models:
|
||||
if self.is_tinygrad_model(model):
|
||||
model_file = MODELS_PATH / f"{model}.thneed"
|
||||
if not model_file.is_file():
|
||||
needs_download = True
|
||||
need_to_update_models = True
|
||||
continue
|
||||
|
||||
expected_size = model_sizes.get(model_file.name)
|
||||
local_size = local_model_sizes.get(model_file.name)
|
||||
local_size = self.model_sizes.get(model_file.name)
|
||||
|
||||
if expected_size > 0 and local_size != expected_size:
|
||||
print(f"Model {model} is outdated. Deleting {model_file}...")
|
||||
delete_file(model_file)
|
||||
needs_download = True
|
||||
need_to_update_models = True
|
||||
else:
|
||||
model_missing = False
|
||||
model_outdated = False
|
||||
|
||||
if needs_download:
|
||||
for filename, _ in TINYGRAD_FILES:
|
||||
expected_file = MODELS_PATH / f"{model}_{filename}"
|
||||
if not expected_file.is_file():
|
||||
model_missing = True
|
||||
need_to_update_models = True
|
||||
break
|
||||
|
||||
for filename, _ in TINYGRAD_FILES:
|
||||
model_file = f"{model}_{filename}"
|
||||
|
||||
expected_size = model_sizes.get(model_file)
|
||||
local_size = self.model_sizes.get(model_file)
|
||||
|
||||
if expected_size > 0 and local_size != expected_size:
|
||||
model_outdated = True
|
||||
need_to_update_models = True
|
||||
break
|
||||
|
||||
if model_missing or model_outdated:
|
||||
print(f"Model {model} is either missing required files or outdated. Deleting...")
|
||||
for filename, _ in TINYGRAD_FILES:
|
||||
delete_file(MODELS_PATH / f"{model}_{filename}")
|
||||
|
||||
if need_to_update_models:
|
||||
params_memory.put_bool(MODEL_DOWNLOAD_ALL_PARAM, True)
|
||||
|
||||
def check_tinygrad(self, repo_url):
|
||||
tinygrad_url = f"{repo_url}/Tinygrad/{TAR_FILE_NAME}"
|
||||
|
||||
expected_size = get_remote_file_size(tinygrad_url, self.session)
|
||||
local_size = int(self.tinygrad_sizes.get(TAR_FILE_NAME, 0))
|
||||
|
||||
if expected_size > 0 and local_size != expected_size:
|
||||
print(f"Tinygrad version {VERSION} is outdated, expected_size: {expected_size}, local_size: {local_size}, flagging for update...")
|
||||
params.put_bool("TinygradUpdateAvailable", True)
|
||||
|
||||
def copy_default_model(self):
|
||||
classic_default_model_path = MODELS_PATH / f"{DEFAULT_CLASSIC_MODEL}.thneed"
|
||||
classic_default_model_path = MODELS_PATH / "wd-40.thneed"
|
||||
source_path = Path(__file__).parents[1] / "classic_modeld/models/supercombo.thneed"
|
||||
if source_path.is_file() and not classic_default_model_path.is_file():
|
||||
if source_path.is_file() and (not classic_default_model_path.is_file() or source_path.stat().st_size != classic_default_model_path.stat().st_size):
|
||||
shutil.copyfile(source_path, classic_default_model_path)
|
||||
print(f"Copied the classic default model from {source_path} to {classic_default_model_path}")
|
||||
self.update_model_size(classic_default_model_path)
|
||||
|
||||
default_model_path = MODELS_PATH / f"{DEFAULT_MODEL}.thneed"
|
||||
default_model_path = MODELS_PATH / "national-public-radio.thneed"
|
||||
source_path = Path(__file__).parents[2] / "selfdrive/modeld/models/supercombo.thneed"
|
||||
if source_path.is_file() and not default_model_path.is_file():
|
||||
if source_path.is_file() and (not default_model_path.is_file() or source_path.stat().st_size != default_model_path.stat().st_size):
|
||||
shutil.copyfile(source_path, default_model_path)
|
||||
print(f"Copied the default model from {source_path} to {default_model_path}")
|
||||
self.update_model_size(default_model_path)
|
||||
|
||||
for filename, description in TINYGRAD_FILES:
|
||||
source = TINYGRAD_MODELD_PATH / "models" / filename
|
||||
target = MODELS_PATH / f"{DEFAULT_TINYGRAD_MODEL}_{filename}"
|
||||
if source.is_file() and not target.is_file():
|
||||
target = MODELS_PATH / f"{DEFAULT_MODEL}_{filename}"
|
||||
if source.is_file() and (not target.is_file() or source.stat().st_size != target.stat().st_size):
|
||||
shutil.copyfile(source, target)
|
||||
print(f"Copied the tinygrad {description} from {source} to {target}")
|
||||
|
||||
@@ -121,16 +166,20 @@ class ModelManager:
|
||||
handle_error(None, "Download cancelled...", "Download cancelled...", MODEL_DOWNLOAD_ALL_PARAM, DOWNLOAD_PROGRESS_PARAM)
|
||||
return
|
||||
|
||||
already_downloaded = [model_file for model_file in MODELS_PATH.iterdir() if model_file.is_file() and model in model_file.name]
|
||||
if self.is_tinygrad_model(model):
|
||||
already_downloaded = (MODELS_PATH / f"{model}.thneed").is_file()
|
||||
else:
|
||||
already_downloaded = all((MODELS_PATH / f"{model}_{filename}").is_file() for filename, _ in TINYGRAD_FILES)
|
||||
|
||||
if already_downloaded:
|
||||
continue
|
||||
|
||||
print(f"Model {model} is not downloaded. Preparing to download...")
|
||||
params_memory.put(DOWNLOAD_PROGRESS_PARAM, f"Downloading \"{self.available_model_names[self.available_models.index(model)]}\"...")
|
||||
|
||||
self.download_model(model)
|
||||
|
||||
params_memory.put(DOWNLOAD_PROGRESS_PARAM, "All models downloaded!")
|
||||
params_memory.remove(MODEL_DOWNLOAD_ALL_PARAM)
|
||||
|
||||
def download_model(self, model_to_download):
|
||||
self.downloading_model = True
|
||||
@@ -141,7 +190,7 @@ class ModelManager:
|
||||
self.downloading_model = False
|
||||
return
|
||||
|
||||
if self.model_versions[self.available_models.index(model_to_download)] in {"v1", "v2", "v3", "v4", "v5", "v6"}:
|
||||
if self.is_tinygrad_model(model_to_download):
|
||||
model_path = MODELS_PATH / f"{model_to_download}.thneed"
|
||||
model_url = f"{repo_url}/Models/{model_to_download}.thneed"
|
||||
|
||||
@@ -149,6 +198,8 @@ class ModelManager:
|
||||
download_file(CANCEL_DOWNLOAD_PARAM, model_path, DOWNLOAD_PROGRESS_PARAM, model_url, MODEL_DOWNLOAD_PARAM, self.session)
|
||||
|
||||
if params_memory.get_bool(CANCEL_DOWNLOAD_PARAM):
|
||||
delete_file(model_path)
|
||||
|
||||
handle_error(None, "Download cancelled...", "Download cancelled...", MODEL_DOWNLOAD_PARAM, DOWNLOAD_PROGRESS_PARAM)
|
||||
self.downloading_model = False
|
||||
return
|
||||
@@ -156,8 +207,10 @@ class ModelManager:
|
||||
if verify_download(model_path, model_url, self.session):
|
||||
print(f"Model {model_to_download} downloaded and verified successfully!")
|
||||
self.update_model_size(model_path)
|
||||
|
||||
params_memory.put(DOWNLOAD_PROGRESS_PARAM, "Downloaded!")
|
||||
params_memory.remove(MODEL_DOWNLOAD_PARAM)
|
||||
|
||||
self.downloading_model = False
|
||||
return
|
||||
|
||||
@@ -166,6 +219,8 @@ class ModelManager:
|
||||
download_file(CANCEL_DOWNLOAD_PARAM, model_path, DOWNLOAD_PROGRESS_PARAM, fallback_url, MODEL_DOWNLOAD_PARAM, self.session)
|
||||
|
||||
if params_memory.get_bool(CANCEL_DOWNLOAD_PARAM):
|
||||
delete_file(model_path)
|
||||
|
||||
handle_error(None, "Download cancelled...", "Download cancelled...", MODEL_DOWNLOAD_PARAM, DOWNLOAD_PROGRESS_PARAM)
|
||||
self.downloading_model = False
|
||||
return
|
||||
@@ -173,40 +228,132 @@ class ModelManager:
|
||||
if verify_download(model_path, fallback_url, self.session):
|
||||
print(f"Model {model_to_download} downloaded and verified successfully from GitLab!")
|
||||
self.update_model_size(model_path)
|
||||
|
||||
params_memory.put(DOWNLOAD_PROGRESS_PARAM, "Downloaded!")
|
||||
params_memory.remove(MODEL_DOWNLOAD_PARAM)
|
||||
|
||||
self.downloading_model = False
|
||||
else:
|
||||
handle_error(model_path, "Verification failed...", "GitLab verification failed", MODEL_DOWNLOAD_PARAM, DOWNLOAD_PROGRESS_PARAM)
|
||||
self.downloading_model = False
|
||||
else:
|
||||
all_model_sizes = self.fetch_all_model_sizes(repo_url) or {}
|
||||
tinygrad_filenames = [f"{model_to_download}_{file_key}" for file_key, _ in TINYGRAD_FILES]
|
||||
|
||||
file_sizes = []
|
||||
file_sources = []
|
||||
|
||||
missing = [name for name in tinygrad_filenames if int(all_model_sizes.get(name, 0)) <= 0]
|
||||
if missing:
|
||||
handle_error(None, "Missing size metadata...", f"Sizes not found for: {', '.join(missing)}...", MODEL_DOWNLOAD_PARAM, DOWNLOAD_PROGRESS_PARAM)
|
||||
self.downloading_model = False
|
||||
return
|
||||
|
||||
for filename in tinygrad_filenames:
|
||||
primary_url = f"{repo_url}/Models/compiled/{filename}"
|
||||
file_size = int(all_model_sizes.get(filename, 0))
|
||||
file_sizes.append(file_size)
|
||||
file_sources.append((primary_url, None))
|
||||
|
||||
downloaded_offset_bytes = 0
|
||||
known_file_sizes = [size for size in file_sizes if size > 0]
|
||||
total_model_bytes = sum(known_file_sizes) if len(known_file_sizes) == len(file_sizes) else 0
|
||||
|
||||
for (file_key, description), part_bytes, (primary_url, fallback_url) in zip(TINYGRAD_FILES, file_sizes, file_sources):
|
||||
filename = f"{model_to_download}_{file_key}"
|
||||
model_path = MODELS_PATH / filename
|
||||
|
||||
print(f"Downloading {description} for model: {model_to_download}")
|
||||
download_file(CANCEL_DOWNLOAD_PARAM, model_path, DOWNLOAD_PROGRESS_PARAM, primary_url, MODEL_DOWNLOAD_PARAM, self.session, offset_bytes=downloaded_offset_bytes, total_bytes=total_model_bytes)
|
||||
|
||||
if params_memory.get_bool(CANCEL_DOWNLOAD_PARAM):
|
||||
delete_file(model_path)
|
||||
|
||||
handle_error(None, "Download cancelled...", "Download cancelled...", MODEL_DOWNLOAD_PARAM, DOWNLOAD_PROGRESS_PARAM)
|
||||
self.downloading_model = False
|
||||
return
|
||||
|
||||
if verify_download(model_path, primary_url, self.session):
|
||||
print(f"{description.capitalize()} for {model_to_download} downloaded and verified successfully!")
|
||||
if total_model_bytes:
|
||||
downloaded_offset_bytes += part_bytes
|
||||
continue
|
||||
|
||||
print(f"Verification failed for {filename}. Retrying from GitLab...")
|
||||
fallback_url = f"{GITLAB_URL}/Models/compiled/{filename}"
|
||||
download_file(CANCEL_DOWNLOAD_PARAM, model_path, DOWNLOAD_PROGRESS_PARAM, fallback_url, MODEL_DOWNLOAD_PARAM, self.session, offset_bytes=downloaded_offset_bytes, total_bytes=total_model_bytes)
|
||||
|
||||
if params_memory.get_bool(CANCEL_DOWNLOAD_PARAM):
|
||||
delete_file(model_path)
|
||||
|
||||
handle_error(None, "Download cancelled...", "Download cancelled...", MODEL_DOWNLOAD_PARAM, DOWNLOAD_PROGRESS_PARAM)
|
||||
self.downloading_model = False
|
||||
return
|
||||
|
||||
if verify_download(model_path, fallback_url, self.session):
|
||||
print(f"{description.capitalize()} for {model_to_download} downloaded and verified successfully from GitLab!")
|
||||
if total_model_bytes:
|
||||
downloaded_offset_bytes += part_bytes
|
||||
else:
|
||||
handle_error(model_path, "Verification failed...", f"GitLab verification failed for {filename}", MODEL_DOWNLOAD_PARAM, DOWNLOAD_PROGRESS_PARAM)
|
||||
self.downloading_model = False
|
||||
return
|
||||
|
||||
print(f"Updating model sizes for {model_to_download}...")
|
||||
for filename, _ in TINYGRAD_FILES:
|
||||
file_path = MODELS_PATH / f"{model_to_download}_{filename}"
|
||||
self.update_model_size(file_path)
|
||||
|
||||
params_memory.put(DOWNLOAD_PROGRESS_PARAM, "Downloaded!")
|
||||
params_memory.remove(MODEL_DOWNLOAD_PARAM)
|
||||
|
||||
self.downloading_model = False
|
||||
|
||||
def fetch_all_model_sizes(self, repo_url):
|
||||
if "github" in repo_url:
|
||||
api_url = f"https://api.github.com/repos/{RESOURCES_REPO}/contents?ref=Models"
|
||||
elif "gitlab" in repo_url:
|
||||
api_url = f"https://gitlab.com/api/v4/projects/{urllib.parse.quote_plus(RESOURCES_REPO)}/repository/tree?ref=Models"
|
||||
else:
|
||||
return {}
|
||||
is_github = "github" in repo_url
|
||||
is_gitlab = "gitlab" in repo_url
|
||||
repo_encoded = quote_plus(RESOURCES_REPO)
|
||||
|
||||
model_sizes = {}
|
||||
try:
|
||||
response = self.session.get(api_url)
|
||||
response.raise_for_status()
|
||||
def fetch_dir_sizes(api_url):
|
||||
sizes = {}
|
||||
print(f"Fetching model metadata: {api_url}")
|
||||
response = self.session.get(api_url, timeout=10)
|
||||
response.raise_for_status()
|
||||
content = response.json()
|
||||
|
||||
model_files = [file for file in response.json() if "." in file["name"]]
|
||||
model_files = [file for file in content if "." in file["name"]]
|
||||
|
||||
if "gitlab" in repo_url:
|
||||
model_sizes = {}
|
||||
for file in model_files:
|
||||
metadata_url = f"https://gitlab.com/api/v4/projects/{urllib.parse.quote_plus(RESOURCES_REPO)}/repository/files/{urllib.parse.quote_plus(file['path'])}/raw?ref=Models"
|
||||
metadata_response = self.session.head(metadata_url)
|
||||
metadata_response.raise_for_status()
|
||||
model_sizes[file["name"]] = int(metadata_response.headers.get("content-length", 0))
|
||||
return model_sizes
|
||||
if is_github:
|
||||
for file in model_files:
|
||||
sizes[file["name"]] = file.get("size", 0)
|
||||
else:
|
||||
for file in model_files:
|
||||
file_path = quote_plus(file["path"])
|
||||
metadata_url = f"https://gitlab.com/api/v4/projects/{repo_encoded}/repository/files/{file_path}/raw?ref=Models"
|
||||
head_response = self.session.head(metadata_url, timeout=10)
|
||||
if head_response.ok:
|
||||
sizes[file["name"]] = int(head_response.headers.get("content-length", 0))
|
||||
return sizes
|
||||
|
||||
if is_github:
|
||||
top_api_url = f"https://api.github.com/repos/{RESOURCES_REPO}/contents?ref=Models"
|
||||
version_api_url = f"https://api.github.com/repos/{RESOURCES_REPO}/contents/compiled?ref=Models"
|
||||
elif is_gitlab:
|
||||
top_api_url = f"https://gitlab.com/api/v4/projects/{repo_encoded}/repository/tree?ref=Models"
|
||||
version_api_url = f"https://gitlab.com/api/v4/projects/{repo_encoded}/repository/tree?path=compiled&ref=Models"
|
||||
else:
|
||||
return {file["name"]: file["size"] for file in model_files if "size" in file}
|
||||
print(f"Unsupported repository URL: {repo_url}")
|
||||
return model_sizes
|
||||
|
||||
except Exception as exception:
|
||||
handle_request_error(f"Failed to fetch model sizes from {'GitHub' if 'github' in repo_url else 'GitLab'}: {exception}", None, None, None)
|
||||
model_sizes.update(fetch_dir_sizes(top_api_url))
|
||||
model_sizes.update(fetch_dir_sizes(version_api_url))
|
||||
|
||||
return model_sizes
|
||||
|
||||
except requests.exceptions.RequestException as e:
|
||||
handle_request_error(f"Failed to fetch model sizes from {'GitHub' if is_github else 'GitLab'}: {e}", None, None, None)
|
||||
return {}
|
||||
|
||||
def fetch_models(self, url, repo_url, boot_run=False):
|
||||
@@ -218,15 +365,13 @@ class ModelManager:
|
||||
if model_info:
|
||||
self.update_model_params(model_info)
|
||||
self.check_models(boot_run, repo_url)
|
||||
self.check_tinygrad(repo_url)
|
||||
except Exception as exception:
|
||||
handle_request_error(exception, None, None, None)
|
||||
return []
|
||||
|
||||
def load_model_sizes(self):
|
||||
if self.model_sizes_path.is_file():
|
||||
with open(self.model_sizes_path) as f:
|
||||
return json.load(f)
|
||||
return {}
|
||||
def is_tinygrad_model(self, model):
|
||||
return self.model_versions[self.available_models.index(model)] in {"v1", "v2", "v3", "v4", "v5", "v6"}
|
||||
|
||||
def update_model_params(self, model_info):
|
||||
self.available_models = [model["id"] for model in model_info]
|
||||
@@ -250,9 +395,136 @@ class ModelManager:
|
||||
self.fetch_models(f"{repo_url}/Versions/model_names_{VERSION}.json", repo_url, boot_run)
|
||||
|
||||
def update_model_size(self, file_path):
|
||||
sizes = self.load_model_sizes()
|
||||
sizes[file_path.name] = file_path.stat().st_size
|
||||
|
||||
with open(self.model_sizes_path, "w") as f:
|
||||
json.dump(sizes, f, indent=2)
|
||||
self.model_sizes[file_path.name] = file_path.stat().st_size
|
||||
update_json_file(self.model_sizes_path, self.model_sizes)
|
||||
print(f"Updated size for {file_path.name} in {self.model_sizes_path.name}")
|
||||
|
||||
def update_tinygrad_size(self, file_path):
|
||||
self.tinygrad_sizes[TAR_FILE_NAME] = file_path.stat().st_size
|
||||
update_json_file(self.tinygrad_sizes_path, self.tinygrad_sizes)
|
||||
print(f"Updated size for {TAR_FILE_NAME} in {self.tinygrad_sizes_path.name}")
|
||||
|
||||
def update_tinygrad(self):
|
||||
repo_url = get_repository_url(self.session)
|
||||
if not repo_url:
|
||||
handle_error(None, "GitHub and GitLab are offline...", "Repository unavailable", None, None)
|
||||
return
|
||||
|
||||
primary_url = f"{repo_url}/Tinygrad/{TAR_FILE_NAME}"
|
||||
fallback_url = f"https://gitlab.com/{RESOURCES_REPO}/-/raw/Tinygrad/{TAR_FILE_NAME}"
|
||||
|
||||
tinygrad_tar_path = Path("/data/tmp/tinygrad.tar.gz")
|
||||
try:
|
||||
print(f"Attempting to download tinygrad from {primary_url}...")
|
||||
download_file(CANCEL_DOWNLOAD_PARAM, tinygrad_tar_path, DOWNLOAD_PROGRESS_PARAM, primary_url, UPDATE_TINYGRAD_PARAM, self.session)
|
||||
|
||||
if params_memory.get_bool(CANCEL_DOWNLOAD_PARAM):
|
||||
delete_file(tinygrad_tar_path)
|
||||
|
||||
handle_error(None, "Tinygrad update cancelled...", "Tinygrad update cancelled...", UPDATE_TINYGRAD_PARAM, DOWNLOAD_PROGRESS_PARAM)
|
||||
params_memory.remove("CancelModelDownload")
|
||||
return
|
||||
|
||||
if not verify_download(tinygrad_tar_path, primary_url, self.session):
|
||||
print(f"Verification failed for {primary_url}. Retrying from GitLab...")
|
||||
download_file(CANCEL_DOWNLOAD_PARAM, tinygrad_tar_path, DOWNLOAD_PROGRESS_PARAM, fallback_url, UPDATE_TINYGRAD_PARAM, self.session)
|
||||
|
||||
if params_memory.get_bool(CANCEL_DOWNLOAD_PARAM):
|
||||
delete_file(tinygrad_tar_path)
|
||||
|
||||
handle_error(None, "Tinygrad update cancelled...", "Tinygrad update cancelled...", UPDATE_TINYGRAD_PARAM, DOWNLOAD_PROGRESS_PARAM)
|
||||
params_memory.remove("CancelModelDownload")
|
||||
return
|
||||
|
||||
if not verify_download(tinygrad_tar_path, fallback_url, self.session):
|
||||
handle_error(tinygrad_tar_path, "Verification Failed", "Tinygrad verification failed", UPDATE_TINYGRAD_PARAM, DOWNLOAD_PROGRESS_PARAM)
|
||||
return
|
||||
|
||||
print("Tinygrad downloaded successfully! Proceeding with installation...")
|
||||
self.update_tinygrad_size(tinygrad_tar_path)
|
||||
|
||||
params_memory.put(DOWNLOAD_PROGRESS_PARAM, "Installing...")
|
||||
|
||||
print("Deleting old tinygrad directories...")
|
||||
delete_file(TINYGRAD_MODELD_PATH)
|
||||
print(f"Removed {TINYGRAD_MODELD_PATH}")
|
||||
delete_file(TINYGRAD_REPO_PATH)
|
||||
print(f"Removed {TINYGRAD_REPO_PATH}")
|
||||
|
||||
extract_tar(tinygrad_tar_path, Path(BASEDIR))
|
||||
|
||||
print("Tinygrad update completed successfully!")
|
||||
|
||||
params.put_bool("TinygradUpdateAvailable", False)
|
||||
|
||||
params_memory.put(DOWNLOAD_PROGRESS_PARAM, "Updated!")
|
||||
params_memory.remove(UPDATE_TINYGRAD_PARAM)
|
||||
|
||||
self.update_tinygrad_models(repo_url)
|
||||
except Exception as exception:
|
||||
handle_error(tinygrad_tar_path, "Update Failed", f"An unexpected error occurred: {exception}", UPDATE_TINYGRAD_PARAM, DOWNLOAD_PROGRESS_PARAM)
|
||||
|
||||
def update_tinygrad_models(self, repo_url=None):
|
||||
print("Updating old Tinygrad models...")
|
||||
|
||||
installed_tinygrad_models = set()
|
||||
for filename, _ in TINYGRAD_FILES:
|
||||
suffix = f"_{filename}"
|
||||
for file_path in MODELS_PATH.glob(f"*{suffix}"):
|
||||
model_name = file_path.name.rsplit(suffix, 1)[0]
|
||||
if model_name in set(self.available_models):
|
||||
installed_tinygrad_models.add(model_name)
|
||||
delete_file(file_path)
|
||||
|
||||
self.copy_default_model()
|
||||
|
||||
update_frogpilot_toggles()
|
||||
|
||||
if repo_url is None:
|
||||
return
|
||||
|
||||
current_model = params.get("Model", encoding="utf-8").removesuffix("_default")
|
||||
|
||||
models_to_redownload = [current_model]
|
||||
models_to_redownload += [model for model in sorted(installed_tinygrad_models) if model != current_model]
|
||||
|
||||
if DEFAULT_MODEL in models_to_redownload:
|
||||
models_to_redownload.remove(DEFAULT_MODEL)
|
||||
|
||||
if models_to_redownload:
|
||||
print(f"Redownloading the following models: {', '.join(models_to_redownload)}")
|
||||
self.fetch_models(f"{repo_url}/Versions/model_names_{VERSION}.json", repo_url, boot_run=True)
|
||||
|
||||
for model in models_to_redownload:
|
||||
if params_memory.get_bool(CANCEL_DOWNLOAD_PARAM):
|
||||
handle_error(None, "Download cancelled...", "Download cancelled...", MODEL_DOWNLOAD_ALL_PARAM, DOWNLOAD_PROGRESS_PARAM)
|
||||
return
|
||||
|
||||
params_memory.put(DOWNLOAD_PROGRESS_PARAM, f"Downloading \"{self.available_model_names[self.available_models.index(model)]}\"...")
|
||||
self.download_model(model)
|
||||
else:
|
||||
print("No previously installed tinygrad models to redownload")
|
||||
|
||||
update_frogpilot_toggles()
|
||||
|
||||
def validate_models(self):
|
||||
current = params.get("Model", encoding="utf-8")
|
||||
default = params_default.get("Model", encoding="utf-8")
|
||||
|
||||
if current.endswith("_default") and current != default:
|
||||
print(f"Model '{current}' does not match default '{default}', resetting...")
|
||||
params.put("Model", default)
|
||||
|
||||
if VERSION_PATH.is_file():
|
||||
version_name = VERSION_PATH.read_text().strip()
|
||||
if version_name != VERSION or int(self.tinygrad_sizes.get(TAR_FILE_NAME, 0)) == 0:
|
||||
self.update_tinygrad_models()
|
||||
|
||||
self.tinygrad_sizes[TAR_FILE_NAME] = DEFAULT_TINYGRAD_SIZE
|
||||
update_json_file(self.tinygrad_sizes_path, self.tinygrad_sizes)
|
||||
print(f"Updated size for {TAR_FILE_NAME} in {self.tinygrad_sizes_path.name}")
|
||||
|
||||
params.remove("TinygradUpdateAvailable")
|
||||
|
||||
VERSION_PATH.write_text(VERSION)
|
||||
print(f"Updated {VERSION_PATH} to {VERSION}")
|
||||
|
||||
|
After Width: | Height: | Size: 130 KiB |
|
Before Width: | Height: | Size: 3.9 KiB |
|
After Width: | Height: | Size: 479 KiB |
|
Before Width: | Height: | Size: 25 KiB |
|
Before Width: | Height: | Size: 14 KiB After Width: | Height: | Size: 14 KiB |
|
After Width: | Height: | Size: 196 KiB |
|
After Width: | Height: | Size: 454 KiB |
|
Before Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 275 KiB |
|
Before Width: | Height: | Size: 16 KiB |
|
After Width: | Height: | Size: 309 KiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 549 KiB |
@@ -0,0 +1 @@
|
||||
../../../selfdrive/assets/images
|
||||
|
Before Width: | Height: | Size: 1.6 KiB |
|
Before Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 957 B |
@@ -0,0 +1 @@
|
||||
../../../selfdrive/assets/sounds
|
||||
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 48 B |
@@ -0,0 +1 @@
|
||||
../../../../selfdrive/assets/img_chffr_wheel.png
|
||||
|
Before Width: | Height: | Size: 16 KiB After Width: | Height: | Size: 48 B |
@@ -8,10 +8,11 @@ import shutil
|
||||
from datetime import date, timedelta
|
||||
from dateutil import easter
|
||||
from pathlib import Path
|
||||
from urllib.parse import quote_plus
|
||||
|
||||
from openpilot.frogpilot.assets.download_functions import GITLAB_URL, download_file, get_repository_url, handle_error, handle_request_error, verify_download
|
||||
from openpilot.frogpilot.common.frogpilot_utilities import delete_file, extract_zip
|
||||
from openpilot.frogpilot.common.frogpilot_variables import ACTIVE_THEME_PATH, RANDOM_EVENTS_PATH, RESOURCES_REPO, THEME_SAVE_PATH, params, params_memory, update_frogpilot_toggles
|
||||
from openpilot.frogpilot.common.frogpilot_utilities import delete_file, extract_zip, load_json_file, update_json_file
|
||||
from openpilot.frogpilot.common.frogpilot_variables import ACTIVE_THEME_PATH, RANDOM_EVENTS_PATH, RESOURCES_REPO, THEME_SAVE_PATH, params, params_memory
|
||||
|
||||
CANCEL_DOWNLOAD_PARAM = "CancelThemeDownload"
|
||||
DOWNLOAD_PROGRESS_PARAM = "ThemeDownloadProgress"
|
||||
@@ -35,184 +36,17 @@ HOLIDAY_SLUGS = {
|
||||
"christmas_week": "Christmas"
|
||||
}
|
||||
|
||||
def calculate_thanksgiving(year):
|
||||
november_first = date(year, 11, 1)
|
||||
days_to_thursday = (3 - november_first.weekday()) % 7
|
||||
first_thursday = november_first + timedelta(days=days_to_thursday)
|
||||
return first_thursday + timedelta(days=21)
|
||||
|
||||
def get_full_themes():
|
||||
theme_packs_path = THEME_SAVE_PATH / "theme_packs"
|
||||
if not theme_packs_path.exists():
|
||||
return []
|
||||
|
||||
valid_themes = set()
|
||||
for theme_directory in theme_packs_path.iterdir():
|
||||
if not theme_directory.is_dir():
|
||||
continue
|
||||
|
||||
base_name = theme_directory.name.replace("-animated", "")
|
||||
|
||||
animated_path = theme_packs_path / f"{base_name}-animated"
|
||||
base_path = theme_packs_path / base_name
|
||||
|
||||
base_valid = all((base_path / asset).is_dir() for asset in {"colors", "sounds"})
|
||||
animated_icons_exist = (animated_path / "icons").is_dir()
|
||||
base_icons_exist = (base_path / "icons").is_dir()
|
||||
|
||||
if base_valid and (animated_icons_exist or base_icons_exist):
|
||||
if animated_icons_exist:
|
||||
valid_themes.add(f"{base_name}-animated")
|
||||
else:
|
||||
valid_themes.add(base_name)
|
||||
|
||||
return sorted(valid_themes)
|
||||
|
||||
def get_holiday_theme_dates(year):
|
||||
return {
|
||||
"new_years": date(year, 1, 1),
|
||||
"valentines_day": date(year, 2, 14),
|
||||
"st_patricks_day": date(year, 3, 17),
|
||||
"world_frog_day": date(year, 3, 20),
|
||||
"april_fools": date(year, 4, 1),
|
||||
"easter_week": easter.easter(year),
|
||||
"may_the_fourth": date(year, 5, 4),
|
||||
"cinco_de_mayo": date(year, 5, 5),
|
||||
"stitch_day": date(year, 6, 26),
|
||||
"fourth_of_july": date(year, 7, 4),
|
||||
"halloween_week": date(year, 10, 31),
|
||||
"thanksgiving_week": calculate_thanksgiving(year),
|
||||
"christmas_week": date(year, 12, 21)
|
||||
}
|
||||
|
||||
def randomize_distance_icons(available_themes, selected_theme):
|
||||
theme_packs_path = THEME_SAVE_PATH / "theme_packs"
|
||||
if not theme_packs_path.exists():
|
||||
return "stock"
|
||||
|
||||
candidates = []
|
||||
for theme_pack in theme_packs_path.iterdir():
|
||||
if not theme_pack.is_dir():
|
||||
continue
|
||||
|
||||
distance_icons_dir = theme_pack / "distance_icons"
|
||||
if not distance_icons_dir.is_dir():
|
||||
continue
|
||||
|
||||
icon_name = theme_pack.name.lower()
|
||||
|
||||
theme_association = [theme for theme in available_themes if theme.replace("-animated", "") in icon_name]
|
||||
if theme_association and selected_theme not in icon_name:
|
||||
continue
|
||||
|
||||
weight = 5 if selected_theme in icon_name else 1
|
||||
candidates.extend([theme_pack.name] * weight)
|
||||
|
||||
return random.choice(candidates) if candidates else "stock"
|
||||
|
||||
def randomize_theme_asset(available_themes):
|
||||
if not available_themes:
|
||||
return "stock"
|
||||
|
||||
return random.choice(available_themes)
|
||||
|
||||
def randomize_wheel_image(available_themes, selected_theme):
|
||||
steering_wheels_path = THEME_SAVE_PATH / "steering_wheels"
|
||||
if not steering_wheels_path.exists():
|
||||
return "stock"
|
||||
|
||||
candidates = []
|
||||
for wheel_file in steering_wheels_path.iterdir():
|
||||
if not wheel_file.is_file():
|
||||
continue
|
||||
|
||||
name = wheel_file.stem.lower()
|
||||
|
||||
theme_association = [theme for theme in available_themes if theme.replace("-animated", "") in name]
|
||||
if theme_association and selected_theme not in name:
|
||||
continue
|
||||
|
||||
weight = 5 if selected_theme in name else 1
|
||||
candidates.extend([wheel_file.stem] * weight)
|
||||
|
||||
return random.choice(candidates) if candidates else "stock"
|
||||
|
||||
def update_theme_asset(asset_type, theme, holiday_theme):
|
||||
save_location = ACTIVE_THEME_PATH / asset_type
|
||||
|
||||
if holiday_theme != "stock":
|
||||
asset_location = HOLIDAY_THEME_PATH / holiday_theme / asset_type
|
||||
elif theme in HOLIDAY_SLUGS:
|
||||
asset_location = HOLIDAY_THEME_PATH / theme / asset_type
|
||||
elif f"{theme}_week" in HOLIDAY_SLUGS:
|
||||
asset_location = HOLIDAY_THEME_PATH / f"{theme}_week" / asset_type
|
||||
else:
|
||||
asset_location = THEME_SAVE_PATH / "theme_packs" / theme / asset_type
|
||||
|
||||
if not asset_location.exists() or theme == "stock":
|
||||
if (STOCKOP_THEME_PATH / asset_type).is_dir():
|
||||
asset_location = STOCKOP_THEME_PATH / asset_type
|
||||
print(f"Using the stock {asset_type[:-1]} instead")
|
||||
else:
|
||||
if save_location.exists() or save_location.is_symlink():
|
||||
if save_location.is_symlink() or save_location.is_file():
|
||||
save_location.unlink()
|
||||
elif save_location.is_dir():
|
||||
shutil.rmtree(save_location)
|
||||
print(f"Using the stock {asset_type[:-1]} instead")
|
||||
return
|
||||
|
||||
if save_location.exists() or save_location.is_symlink():
|
||||
if save_location.is_symlink() or save_location.is_file():
|
||||
save_location.unlink()
|
||||
elif save_location.is_dir():
|
||||
shutil.rmtree(save_location)
|
||||
|
||||
save_location.parent.mkdir(parents=True, exist_ok=True)
|
||||
save_location.symlink_to(asset_location, target_is_directory=True)
|
||||
print(f"Linked {save_location} to {asset_location}")
|
||||
|
||||
def update_wheel_image(image, holiday_theme="stock", random_event=True):
|
||||
wheel_save_location = ACTIVE_THEME_PATH / "steering_wheel"
|
||||
|
||||
if holiday_theme != "stock":
|
||||
wheel_location = HOLIDAY_THEME_PATH / holiday_theme / "steering_wheel"
|
||||
elif random_event:
|
||||
wheel_location = RANDOM_EVENTS_PATH / "steering_wheels"
|
||||
elif image == "stock":
|
||||
wheel_location = STOCKOP_THEME_PATH / "steering_wheel"
|
||||
elif image in HOLIDAY_SLUGS:
|
||||
wheel_location = HOLIDAY_THEME_PATH / image / "steering_wheel"
|
||||
elif f"{image}_week" in HOLIDAY_SLUGS:
|
||||
wheel_location = HOLIDAY_THEME_PATH / f"{image}_week" / "steering_wheel"
|
||||
else:
|
||||
wheel_location = THEME_SAVE_PATH / "steering_wheels"
|
||||
|
||||
if not wheel_location.exists():
|
||||
wheel_location = STOCKOP_THEME_PATH / "steering_wheel"
|
||||
print("Using the stock steering wheel instead")
|
||||
|
||||
if wheel_save_location.exists():
|
||||
if wheel_save_location.is_symlink():
|
||||
wheel_save_location.unlink()
|
||||
elif wheel_save_location.is_dir():
|
||||
shutil.rmtree(wheel_save_location)
|
||||
|
||||
wheel_save_location.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
image_name = image.replace(" ", "_").lower()
|
||||
matching_files = [images for images in wheel_location.iterdir() if images.stem.lower() in {image_name, "wheel"}]
|
||||
if matching_files:
|
||||
source_file = matching_files[0]
|
||||
destination_file = wheel_save_location / f"wheel{source_file.suffix}"
|
||||
|
||||
if destination_file.exists():
|
||||
destination_file.unlink()
|
||||
destination_file.symlink_to(source_file)
|
||||
print(f"Linked {destination_file} to {source_file}")
|
||||
THEME_COMPONENT_PARAMS = {
|
||||
"colors": "ColorToDownload",
|
||||
"distance_icons": "DistanceIconToDownload",
|
||||
"icons": "IconToDownload",
|
||||
"signals": "SignalToDownload",
|
||||
"sounds": "SoundToDownload",
|
||||
"steering_wheels": "WheelToDownload"
|
||||
}
|
||||
|
||||
class ThemeManager:
|
||||
def __init__(self):
|
||||
def __init__(self, boot_run=False):
|
||||
self.downloading_theme = False
|
||||
self.theme_updated = False
|
||||
|
||||
@@ -220,24 +54,365 @@ class ThemeManager:
|
||||
|
||||
self.previous_asset_mappings = {}
|
||||
|
||||
self.theme_sizes_path = THEME_SAVE_PATH / "theme_sizes.json"
|
||||
|
||||
self.theme_sizes = load_json_file(self.theme_sizes_path)
|
||||
|
||||
self.session = requests.Session()
|
||||
self.session.headers.update({"Accept-Language": "en"})
|
||||
self.session.headers.update({"User-Agent": "frogpilot-theme-downloader/1.0 (https://github.com/FrogAi/FrogPilot)"})
|
||||
|
||||
if boot_run:
|
||||
self.copy_default_theme()
|
||||
|
||||
@staticmethod
|
||||
def calculate_thanksgiving(year):
|
||||
november_first = date(year, 11, 1)
|
||||
days_to_thursday = (3 - november_first.weekday()) % 7
|
||||
first_thursday = november_first + timedelta(days=days_to_thursday)
|
||||
return first_thursday + timedelta(days=21)
|
||||
|
||||
@staticmethod
|
||||
def copy_default_theme():
|
||||
world_frog_day_theme_path = HOLIDAY_THEME_PATH / "world_frog_day"
|
||||
|
||||
for theme_subfolder_name, save_subfolder_path in [
|
||||
("colors", "theme_packs/frog/colors"),
|
||||
("distance_icons", "theme_packs/frog-animated/distance_icons"),
|
||||
("icons", "theme_packs/frog-animated/icons"),
|
||||
("signals", "theme_packs/frog/signals"),
|
||||
("sounds", "theme_packs/frog/sounds"),
|
||||
]:
|
||||
source_folder_path = world_frog_day_theme_path / theme_subfolder_name
|
||||
destination_folder_path = THEME_SAVE_PATH / save_subfolder_path
|
||||
destination_folder_path.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copytree(source_folder_path, destination_folder_path, dirs_exist_ok=True)
|
||||
|
||||
steering_wheel_image_path = world_frog_day_theme_path / "steering_wheel/wheel.png"
|
||||
steering_wheel_save_path = THEME_SAVE_PATH / "steering_wheels/frog.png"
|
||||
steering_wheel_save_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copy2(steering_wheel_image_path, steering_wheel_save_path)
|
||||
|
||||
def download_theme(self, theme_component, theme_name, asset_param, frogpilot_toggles):
|
||||
self.downloading_theme = True
|
||||
|
||||
repo_url = get_repository_url(self.session)
|
||||
if not repo_url:
|
||||
handle_error(None, "GitHub and GitLab are offline...", "Repository unavailable", asset_param, DOWNLOAD_PROGRESS_PARAM)
|
||||
self.downloading_theme = False
|
||||
return
|
||||
|
||||
if theme_component == "distance_icons":
|
||||
download_link = f"{repo_url}/Distance-Icons/{theme_name}"
|
||||
download_path = THEME_SAVE_PATH / "theme_packs" / theme_name / theme_component
|
||||
extensions = [".zip"]
|
||||
elif theme_component == "steering_wheels":
|
||||
download_link = f"{repo_url}/Steering-Wheels/{theme_name}"
|
||||
download_path = THEME_SAVE_PATH / theme_component / theme_name
|
||||
extensions = [".gif", ".png"]
|
||||
else:
|
||||
download_link = f"{repo_url}/Themes/{theme_name}/{theme_component}"
|
||||
download_path = THEME_SAVE_PATH / "theme_packs" / theme_name / theme_component
|
||||
extensions = [".zip"]
|
||||
|
||||
for extension in extensions:
|
||||
theme_path = download_path.with_suffix(extension)
|
||||
theme_url = download_link + extension
|
||||
|
||||
delete_file(theme_path)
|
||||
|
||||
print(f"Downloading theme from GitHub: {theme_name}")
|
||||
download_file(CANCEL_DOWNLOAD_PARAM, theme_path, DOWNLOAD_PROGRESS_PARAM, theme_url, asset_param, self.session)
|
||||
|
||||
if params_memory.get_bool(CANCEL_DOWNLOAD_PARAM):
|
||||
delete_file(theme_path)
|
||||
handle_error(None, "Download cancelled...", "Download cancelled...", asset_param, DOWNLOAD_PROGRESS_PARAM)
|
||||
|
||||
self.downloading_theme = False
|
||||
return
|
||||
|
||||
if verify_download(theme_path, theme_url, self.session):
|
||||
print(f"Theme {theme_name} downloaded and verified successfully from GitHub!")
|
||||
self.update_theme_size(theme_component, theme_name, theme_path.stat().st_size)
|
||||
|
||||
if extension == ".zip":
|
||||
params_memory.put(DOWNLOAD_PROGRESS_PARAM, "Unpacking theme...")
|
||||
extract_zip(theme_path, download_path)
|
||||
|
||||
params_memory.put(DOWNLOAD_PROGRESS_PARAM, "Downloaded!")
|
||||
params_memory.remove(asset_param)
|
||||
|
||||
self.downloading_theme = False
|
||||
|
||||
self.update_themes(frogpilot_toggles)
|
||||
return
|
||||
elif self.handle_verification_failure(extension, theme_component, theme_name, asset_param, theme_path, download_path, frogpilot_toggles):
|
||||
return
|
||||
|
||||
handle_error(download_path, "Download failed...", "Download failed...", asset_param, DOWNLOAD_PROGRESS_PARAM)
|
||||
self.downloading_theme = False
|
||||
|
||||
def fetch_assets(self, repo_url, frogpilot_toggles):
|
||||
is_github = "github" in repo_url
|
||||
is_gitlab = "gitlab" in repo_url
|
||||
|
||||
repo_encoded = quote_plus(RESOURCES_REPO)
|
||||
|
||||
assets = {"themes": {}, "wheels": []}
|
||||
try:
|
||||
def list_files(branch):
|
||||
if is_github:
|
||||
response = self.session.get(f"https://api.github.com/repos/{RESOURCES_REPO}/git/trees/{branch}?recursive=1", timeout=10)
|
||||
response.raise_for_status()
|
||||
return [
|
||||
{
|
||||
"path": item.get("path", ""),
|
||||
"name": Path(item.get("path", "")).name,
|
||||
"type": item.get("type"),
|
||||
"size": item.get("size", 0),
|
||||
}
|
||||
for item in response.json().get("tree", [])
|
||||
if item.get("type") == "blob"
|
||||
]
|
||||
if is_gitlab:
|
||||
response = self.session.get(f"https://gitlab.com/api/v4/projects/{repo_encoded}/repository/tree?ref={branch}&recursive=true", timeout=10)
|
||||
response.raise_for_status()
|
||||
return [
|
||||
{
|
||||
"path": item.get("path", ""),
|
||||
"name": item.get("name", ""),
|
||||
"type": item.get("type"),
|
||||
"size": 0,
|
||||
}
|
||||
for item in response.json()
|
||||
if item.get("type") in ("blob", "file")
|
||||
]
|
||||
print(f"Unsupported repository URL: {repo_url}")
|
||||
return []
|
||||
|
||||
def file_size(branch, path, fallback):
|
||||
if is_github:
|
||||
return int(fallback or 0)
|
||||
response = self.session.head(f"https://gitlab.com/api/v4/projects/{repo_encoded}/repository/files/{quote_plus(path)}/raw?ref={branch}", timeout=10)
|
||||
return int(response.headers.get("content-length", 0)) if response.ok else 0
|
||||
|
||||
for branch in ["Distance-Icons", "Steering-Wheels"]:
|
||||
for item in list_files(branch):
|
||||
if item.get("type") not in ("file", "blob"):
|
||||
continue
|
||||
|
||||
path = item["path"]
|
||||
size = file_size(branch, path, item.get("size", 0))
|
||||
|
||||
if branch == "Steering-Wheels":
|
||||
assets["wheels"].append(path)
|
||||
theme_name = Path(path).stem
|
||||
local_files = list((THEME_SAVE_PATH / "steering_wheels").glob(f"{theme_name}.*"))
|
||||
if local_files and size > 0:
|
||||
local_size = self.theme_sizes.get("wheels", {}).get(theme_name)
|
||||
if local_size != size:
|
||||
self.download_theme("steering_wheels", theme_name, THEME_COMPONENT_PARAMS["steering_wheels"], frogpilot_toggles)
|
||||
|
||||
elif branch == "Distance-Icons":
|
||||
component_name = "distance_icons"
|
||||
theme_name = Path(path).stem
|
||||
assets["themes"].setdefault(theme_name, set()).add(component_name)
|
||||
|
||||
local_path = THEME_SAVE_PATH / "theme_packs" / theme_name / component_name
|
||||
if local_path.exists() and size > 0:
|
||||
local_size = self.theme_sizes.get("themes", {}).get(theme_name, {}).get(component_name)
|
||||
if local_size != size:
|
||||
self.download_theme(component_name, theme_name, THEME_COMPONENT_PARAMS[component_name], frogpilot_toggles)
|
||||
|
||||
branch = "Themes"
|
||||
for item in list_files(branch):
|
||||
if item.get("type") not in ("file", "blob") or "/" not in item["path"]:
|
||||
continue
|
||||
|
||||
expected_size = file_size(branch, item["path"], item.get("size", 0))
|
||||
|
||||
theme_name, sub_path = item["path"].split("/", 1)
|
||||
theme_path = sub_path.lower()
|
||||
|
||||
for key in ("colors", "icons", "signals", "sounds"):
|
||||
if key in theme_path:
|
||||
assets["themes"].setdefault(theme_name, set()).add(key)
|
||||
|
||||
local_path = THEME_SAVE_PATH / "theme_packs" / theme_name / key
|
||||
if local_path.exists():
|
||||
local_size = self.theme_sizes.get("themes", {}).get(theme_name, {}).get(key)
|
||||
if local_size != expected_size:
|
||||
print(f"{key} {theme_name} is outdated, redownloading...")
|
||||
self.download_theme(key, theme_name, THEME_COMPONENT_PARAMS[key], frogpilot_toggles)
|
||||
break
|
||||
|
||||
assets["themes"] = {key: sorted(list(value)) for key, value in assets["themes"].items()}
|
||||
assets["wheels"].sort()
|
||||
return assets
|
||||
|
||||
except requests.exceptions.RequestException as error:
|
||||
print(f"Request failed: {error}")
|
||||
handle_request_error(f"Failed to fetch theme sizes from {'GitHub' if is_github else 'GitLab'}: {error}", None, None, None)
|
||||
return {}
|
||||
|
||||
@staticmethod
|
||||
def format_name(name, component):
|
||||
base = Path(name).stem
|
||||
creator = ""
|
||||
if "~" in base:
|
||||
base, creator = base.split("~", 1)
|
||||
|
||||
parts = base.replace("_", "-").split("-")
|
||||
capitalized_parts = [part.capitalize() for part in parts if part]
|
||||
|
||||
if len(capitalized_parts) > 1 and component != "steering_wheels":
|
||||
display = f"{capitalized_parts[0]} ({' '.join(capitalized_parts[1:])})"
|
||||
else:
|
||||
display = " ".join(capitalized_parts)
|
||||
|
||||
if creator:
|
||||
return f"{display} - by: {creator}"
|
||||
return display
|
||||
|
||||
@staticmethod
|
||||
def get_full_themes():
|
||||
theme_packs_path = THEME_SAVE_PATH / "theme_packs"
|
||||
if not theme_packs_path.exists():
|
||||
return []
|
||||
|
||||
valid_themes = set()
|
||||
for theme_directory in theme_packs_path.iterdir():
|
||||
if not theme_directory.is_dir():
|
||||
continue
|
||||
|
||||
base_name = theme_directory.name.replace("-animated", "")
|
||||
|
||||
animated_path = theme_packs_path / f"{base_name}-animated"
|
||||
base_path = theme_packs_path / base_name
|
||||
|
||||
base_valid = all((base_path / asset).is_dir() for asset in {"colors", "sounds"})
|
||||
animated_icons_exist = (animated_path / "icons").is_dir()
|
||||
base_icons_exist = (base_path / "icons").is_dir()
|
||||
|
||||
if base_valid and (animated_icons_exist or base_icons_exist):
|
||||
if animated_icons_exist:
|
||||
valid_themes.add(f"{base_name}-animated")
|
||||
else:
|
||||
valid_themes.add(base_name)
|
||||
|
||||
return sorted(valid_themes)
|
||||
|
||||
@staticmethod
|
||||
def get_holiday_theme_dates(year):
|
||||
return {
|
||||
"new_years": date(year, 1, 1),
|
||||
"valentines_day": date(year, 2, 14),
|
||||
"st_patricks_day": date(year, 3, 17),
|
||||
"world_frog_day": date(year, 3, 20),
|
||||
"april_fools": date(year, 4, 1),
|
||||
"easter_week": easter.easter(year),
|
||||
"may_the_fourth": date(year, 5, 4),
|
||||
"cinco_de_mayo": date(year, 5, 5),
|
||||
"stitch_day": date(year, 6, 26),
|
||||
"fourth_of_july": date(year, 7, 4),
|
||||
"halloween_week": date(year, 10, 31),
|
||||
"thanksgiving_week": ThemeManager.calculate_thanksgiving(year),
|
||||
"christmas_week": date(year, 12, 21)
|
||||
}
|
||||
|
||||
def handle_verification_failure(self, extension, theme_component, theme_name, asset_param, theme_path, download_path, frogpilot_toggles):
|
||||
if theme_component == "distance_icons":
|
||||
download_link = f"{GITLAB_URL}/Distance-Icons/{theme_name}"
|
||||
elif theme_component == "steering_wheels":
|
||||
download_link = f"{GITLAB_URL}/Steering-Wheels/{theme_name}"
|
||||
else:
|
||||
download_link = f"{GITLAB_URL}/Themes/{theme_name}/{theme_component}"
|
||||
|
||||
delete_file(theme_path)
|
||||
|
||||
theme_url = download_link + extension
|
||||
print(f"Downloading theme from GitLab: {theme_name}")
|
||||
download_file(CANCEL_DOWNLOAD_PARAM, theme_path, DOWNLOAD_PROGRESS_PARAM, theme_url, asset_param, self.session)
|
||||
|
||||
if verify_download(theme_path, theme_url, self.session):
|
||||
print(f"Theme {theme_name} downloaded and verified successfully from GitLab!")
|
||||
self.update_theme_size(theme_component, theme_name, theme_path.stat().st_size)
|
||||
|
||||
if extension == ".zip":
|
||||
params_memory.put(DOWNLOAD_PROGRESS_PARAM, "Unpacking theme...")
|
||||
extract_zip(theme_path, download_path)
|
||||
|
||||
params_memory.put(DOWNLOAD_PROGRESS_PARAM, "Downloaded!")
|
||||
params_memory.remove(asset_param)
|
||||
|
||||
self.downloading_theme = False
|
||||
|
||||
self.update_themes(frogpilot_toggles)
|
||||
return True
|
||||
|
||||
handle_error(None, "Download failed...", "Download failed...", asset_param, DOWNLOAD_PROGRESS_PARAM)
|
||||
self.downloading_theme = False
|
||||
return False
|
||||
|
||||
@staticmethod
|
||||
def is_within_week_of(target_date, current_date):
|
||||
start_of_week = target_date - timedelta(days=target_date.weekday())
|
||||
return start_of_week <= current_date < target_date
|
||||
|
||||
def update_holiday(self):
|
||||
current_date = date.today()
|
||||
@staticmethod
|
||||
def randomize_distance_icons(available_themes, selected_theme):
|
||||
theme_packs_path = THEME_SAVE_PATH / "theme_packs"
|
||||
if not theme_packs_path.exists():
|
||||
return "stock"
|
||||
|
||||
holidays = get_holiday_theme_dates(current_date.year)
|
||||
for holiday, holiday_date in holidays.items():
|
||||
if (holiday.endswith("_week") and self.is_within_week_of(holiday_date, current_date)) or (current_date == holiday_date):
|
||||
return holiday
|
||||
candidates = []
|
||||
for theme_pack in theme_packs_path.iterdir():
|
||||
if not theme_pack.is_dir():
|
||||
continue
|
||||
|
||||
return "stock"
|
||||
distance_icons_dir = theme_pack / "distance_icons"
|
||||
if not distance_icons_dir.is_dir():
|
||||
continue
|
||||
|
||||
icon_name = theme_pack.name.lower()
|
||||
|
||||
theme_association = [theme for theme in available_themes if theme.replace("-animated", "") in icon_name]
|
||||
if theme_association and selected_theme not in icon_name:
|
||||
continue
|
||||
|
||||
weight = 5 if selected_theme in icon_name else 1
|
||||
candidates.extend([theme_pack.name] * weight)
|
||||
|
||||
return random.choice(candidates) if candidates else "stock"
|
||||
|
||||
@staticmethod
|
||||
def randomize_theme_asset(available_themes):
|
||||
if not available_themes:
|
||||
return "stock"
|
||||
|
||||
return random.choice(available_themes)
|
||||
|
||||
@staticmethod
|
||||
def randomize_wheel_image(available_themes, selected_theme):
|
||||
steering_wheels_path = THEME_SAVE_PATH / "steering_wheels"
|
||||
if not steering_wheels_path.exists():
|
||||
return "stock"
|
||||
|
||||
candidates = []
|
||||
for wheel_file in steering_wheels_path.iterdir():
|
||||
if not wheel_file.is_file():
|
||||
continue
|
||||
|
||||
name = wheel_file.stem.lower()
|
||||
|
||||
theme_association = [theme for theme in available_themes if theme.replace("-animated", "") in name]
|
||||
if theme_association and selected_theme not in name:
|
||||
continue
|
||||
|
||||
weight = 5 if selected_theme in name else 1
|
||||
candidates.extend([wheel_file.stem] * weight)
|
||||
|
||||
return random.choice(candidates) if candidates else "stock"
|
||||
|
||||
def update_active_theme(self, time_validated, frogpilot_toggles, boot_run=False, randomize_theme=False):
|
||||
if time_validated and frogpilot_toggles.holiday_themes:
|
||||
@@ -255,16 +430,16 @@ class ThemeManager:
|
||||
"wheel_image": ("wheel_image", self.holiday_theme)
|
||||
}
|
||||
elif (boot_run or randomize_theme) and frogpilot_toggles.random_themes:
|
||||
available_themes = get_full_themes()
|
||||
selected_theme = randomize_theme_asset(available_themes)
|
||||
available_themes = self.get_full_themes()
|
||||
selected_theme = self.randomize_theme_asset(available_themes)
|
||||
|
||||
asset_mappings = {
|
||||
"color_scheme": ("colors", selected_theme.replace("-animated", "")),
|
||||
"distance_icons": ("distance_icons", randomize_distance_icons(available_themes, selected_theme.replace("-animated", ""))),
|
||||
"distance_icons": ("distance_icons", self.randomize_distance_icons(available_themes, selected_theme.replace("-animated", ""))),
|
||||
"icon_pack": ("icons", selected_theme),
|
||||
"sound_pack": ("sounds", selected_theme.replace("-animated", "")),
|
||||
"turn_signal_pack": ("signals", selected_theme.replace("-animated", "")),
|
||||
"wheel_image": ("wheel_image", randomize_wheel_image(available_themes, selected_theme.replace("-animated", "")))
|
||||
"wheel_image": ("wheel_image", self.randomize_wheel_image(available_themes, selected_theme.replace("-animated", "")))
|
||||
}
|
||||
|
||||
elif not frogpilot_toggles.random_themes:
|
||||
@@ -284,150 +459,55 @@ class ThemeManager:
|
||||
print(f"Updating {asset}: {asset_type} with value {current_value}")
|
||||
|
||||
if asset_type == "wheel_image":
|
||||
update_wheel_image(current_value, self.holiday_theme, random_event=False)
|
||||
self.update_wheel_image(current_value, boot_run=boot_run)
|
||||
else:
|
||||
update_theme_asset(asset_type, current_value, self.holiday_theme)
|
||||
self.update_theme_asset(asset_type, current_value, boot_run=boot_run)
|
||||
|
||||
self.previous_asset_mappings = asset_mappings
|
||||
|
||||
self.theme_updated = True
|
||||
|
||||
def handle_verification_failure(self, ext, theme_component, theme_name, theme_param, theme_path, download_path):
|
||||
if theme_component == "steering_wheels":
|
||||
download_link = f"{GITLAB_URL}/Steering-Wheels/{theme_name}"
|
||||
def update_holiday(self):
|
||||
current_date = date.today()
|
||||
|
||||
holidays = self.get_holiday_theme_dates(current_date.year)
|
||||
for holiday, holiday_date in holidays.items():
|
||||
if (holiday.endswith("_week") and self.is_within_week_of(holiday_date, current_date)) or (current_date == holiday_date):
|
||||
return holiday
|
||||
|
||||
return "stock"
|
||||
|
||||
def update_theme_asset(self, asset_type, theme, boot_run=False):
|
||||
save_location = ACTIVE_THEME_PATH / asset_type
|
||||
|
||||
if self.holiday_theme != "stock":
|
||||
asset_location = HOLIDAY_THEME_PATH / self.holiday_theme / asset_type
|
||||
elif theme in HOLIDAY_SLUGS:
|
||||
asset_location = HOLIDAY_THEME_PATH / theme / asset_type
|
||||
elif f"{theme}_week" in HOLIDAY_SLUGS:
|
||||
asset_location = HOLIDAY_THEME_PATH / f"{theme}_week" / asset_type
|
||||
else:
|
||||
download_link = f"{GITLAB_URL}/Themes/{theme_name}/{theme_component}"
|
||||
asset_location = THEME_SAVE_PATH / "theme_packs" / theme / asset_type
|
||||
|
||||
if theme_path.is_file():
|
||||
delete_file(theme_path)
|
||||
if not asset_location.exists() or theme == "stock":
|
||||
asset_location = STOCKOP_THEME_PATH / asset_type
|
||||
print(f"Using the stock {asset_type[:-1]} instead")
|
||||
|
||||
theme_url = download_link + ext
|
||||
print(f"Downloading theme from GitLab: {theme_name}")
|
||||
download_file(CANCEL_DOWNLOAD_PARAM, theme_path, DOWNLOAD_PROGRESS_PARAM, theme_url, theme_param, self.session)
|
||||
delete_file(save_location, print_error=not boot_run)
|
||||
|
||||
if params_memory.get_bool(CANCEL_DOWNLOAD_PARAM):
|
||||
handle_error(None, "Download cancelled...", "Download cancelled...", theme_param, DOWNLOAD_PROGRESS_PARAM)
|
||||
self.downloading_theme = False
|
||||
return
|
||||
|
||||
if verify_download(theme_path, theme_url, self.session):
|
||||
print(f"Theme {theme_name} downloaded and verified successfully from GitLab!")
|
||||
if ext == ".zip":
|
||||
params_memory.put(DOWNLOAD_PROGRESS_PARAM, "Unpacking theme...")
|
||||
extract_zip(theme_path, download_path)
|
||||
params_memory.put(DOWNLOAD_PROGRESS_PARAM, "Downloaded!")
|
||||
return True
|
||||
else:
|
||||
return False
|
||||
|
||||
def download_theme(self, theme_component, theme_name, theme_param):
|
||||
self.downloading_theme = True
|
||||
|
||||
repo_url = get_repository_url(self.session)
|
||||
if not repo_url:
|
||||
handle_error(None, "GitHub and GitLab are offline...", "Repository unavailable", theme_param, DOWNLOAD_PROGRESS_PARAM)
|
||||
self.downloading_theme = False
|
||||
return
|
||||
|
||||
if theme_component == "steering_wheels":
|
||||
download_link = f"{repo_url}/Steering-Wheels/{theme_name}"
|
||||
download_path = THEME_SAVE_PATH / theme_component / theme_name
|
||||
extensions = [".gif", ".png"]
|
||||
else:
|
||||
download_link = f"{repo_url}/Themes/{theme_name}/{theme_component}"
|
||||
download_path = THEME_SAVE_PATH / "theme_packs" / theme_name / theme_component
|
||||
extensions = [".zip"]
|
||||
|
||||
for ext in extensions:
|
||||
theme_path = download_path.with_suffix(ext)
|
||||
if theme_path.is_file():
|
||||
delete_file(theme_path)
|
||||
|
||||
theme_url = download_link + ext
|
||||
print(f"Downloading theme from GitHub: {theme_name}")
|
||||
download_file(CANCEL_DOWNLOAD_PARAM, theme_path, DOWNLOAD_PROGRESS_PARAM, theme_url, theme_param, self.session)
|
||||
|
||||
if params_memory.get_bool(CANCEL_DOWNLOAD_PARAM):
|
||||
handle_error(None, "Download cancelled...", "Download cancelled...", theme_param, DOWNLOAD_PROGRESS_PARAM)
|
||||
self.downloading_theme = False
|
||||
return
|
||||
|
||||
if verify_download(theme_path, theme_url, self.session):
|
||||
print(f"Theme {theme_name} downloaded and verified successfully from GitHub!")
|
||||
if ext == ".zip":
|
||||
params_memory.put(DOWNLOAD_PROGRESS_PARAM, "Unpacking theme...")
|
||||
extract_zip(theme_path, download_path)
|
||||
params_memory.put(DOWNLOAD_PROGRESS_PARAM, "Downloaded!")
|
||||
self.downloading_theme = False
|
||||
return
|
||||
elif self.handle_verification_failure(ext, theme_component, theme_name, theme_param, theme_path, download_path):
|
||||
self.downloading_theme = False
|
||||
return
|
||||
|
||||
handle_error(download_path, "Download failed...", "Download failed...", theme_param, DOWNLOAD_PROGRESS_PARAM)
|
||||
self.downloading_theme = False
|
||||
|
||||
def fetch_assets(self, repo_url):
|
||||
branches = ["Distance-Icons", "Steering-Wheels", "Themes"]
|
||||
|
||||
assets = {
|
||||
"themes": {},
|
||||
"wheels": []
|
||||
}
|
||||
|
||||
try:
|
||||
for branch in branches:
|
||||
if "github" in repo_url:
|
||||
api_url = f"https://api.github.com/repos/{RESOURCES_REPO}/git/trees/{branch}?recursive=1"
|
||||
elif "gitlab" in repo_url:
|
||||
api_url = f"https://gitlab.com/api/v4/projects/{RESOURCES_REPO.replace('/', '%2F')}/repository/tree?ref={branch}&recursive=true"
|
||||
else:
|
||||
print(f"Unsupported repository URL: {repo_url}")
|
||||
return assets
|
||||
|
||||
print(f"Fetching assets from branch '{branch}': {api_url}")
|
||||
response = self.session.get(api_url, timeout=10)
|
||||
response.raise_for_status()
|
||||
content = response.json()
|
||||
|
||||
if "github" in repo_url:
|
||||
content = content.get("tree", [])
|
||||
|
||||
for item in content:
|
||||
if item["type"] != "blob":
|
||||
continue
|
||||
|
||||
if branch == "Steering-Wheels":
|
||||
assets["wheels"].append(item["path"])
|
||||
elif branch == "Themes":
|
||||
theme_name = item["path"].split("/")[0]
|
||||
assets["themes"].setdefault(theme_name, set())
|
||||
|
||||
item_path = item["path"].lower()
|
||||
if "colors" in item_path:
|
||||
assets["themes"][theme_name].add("colors")
|
||||
elif "distance_icons" in item_path:
|
||||
assets["themes"][theme_name].add("distance_icons")
|
||||
elif "icons" in item_path:
|
||||
assets["themes"][theme_name].add("icons")
|
||||
elif "signals" in item_path:
|
||||
assets["themes"][theme_name].add("signals")
|
||||
elif "sounds" in item_path:
|
||||
assets["themes"][theme_name].add("sounds")
|
||||
|
||||
return {**assets, "themes": {k: list(v) for k, v in assets["themes"].items()}}
|
||||
except requests.exceptions.RequestException as error:
|
||||
handle_request_error(f"Failed to fetch theme sizes from {'GitHub' if 'github' in repo_url else 'GitLab'}: {error}", None, None, None)
|
||||
return {}
|
||||
save_location.parent.mkdir(parents=True, exist_ok=True)
|
||||
save_location.symlink_to(asset_location, target_is_directory=True)
|
||||
print(f"Linked {save_location} to {asset_location}")
|
||||
|
||||
def update_theme_params(self, downloadable_colors, downloadable_distance_icons, downloadable_icons, downloadable_signals, downloadable_sounds, downloadable_wheels):
|
||||
def update_param(key, assets, subfolder):
|
||||
if subfolder == "steering_wheels":
|
||||
themes_path = THEME_SAVE_PATH / subfolder
|
||||
existing_assets = {item.stem.replace("_", " ").title() for item in themes_path.glob("*") if item.is_file()}
|
||||
existing_assets = {self.format_name(item.name, "steering_wheels") for item in themes_path.glob("*") if item.is_file()}
|
||||
else:
|
||||
themes_path = THEME_SAVE_PATH / "theme_packs"
|
||||
existing_assets = {item.parent.name.replace("_", " ").title() for item in themes_path.glob(f"*/{subfolder}") if item.is_dir()}
|
||||
existing_assets = {self.format_name(item.parent.name, subfolder) for item in themes_path.glob(f"*/{subfolder}") if item.is_dir()}
|
||||
|
||||
params.put(key, ",".join(sorted(set(assets) - existing_assets)))
|
||||
print(f"{key} updated successfully")
|
||||
|
||||
@@ -438,56 +518,46 @@ class ThemeManager:
|
||||
update_param("DownloadableSounds", downloadable_sounds, "sounds")
|
||||
update_param("DownloadableWheels", downloadable_wheels, "steering_wheels")
|
||||
|
||||
def validate_themes(self, downloadable_colors, downloadable_distance_icons, downloadable_icons, downloadable_signals, downloadable_sounds, downloadable_wheels, frogpilot_toggles):
|
||||
asset_mappings = {
|
||||
"CustomColors": ("colors", frogpilot_toggles.color_scheme, downloadable_colors),
|
||||
"CustomDistanceIcons": ("distance_icons", frogpilot_toggles.distance_icons, downloadable_distance_icons),
|
||||
"CustomIcons": ("icons", frogpilot_toggles.icon_pack, downloadable_icons),
|
||||
"CustomSounds": ("sounds", frogpilot_toggles.sound_pack, downloadable_sounds),
|
||||
"CustomSignals": ("signals", frogpilot_toggles.signal_icons, downloadable_signals),
|
||||
"WheelIcon": ("steering_wheels", frogpilot_toggles.wheel_image, downloadable_wheels)
|
||||
}
|
||||
downloaded_themes = {}
|
||||
for theme_dir in (THEME_SAVE_PATH / "theme_packs").iterdir():
|
||||
components = []
|
||||
for component in ["colors", "distance_icons", "icons", "signals", "sounds"]:
|
||||
if (theme_dir / component).is_dir():
|
||||
components.append(component)
|
||||
|
||||
for theme_param, (theme_component, theme_name, downloadable_list) in asset_mappings.items():
|
||||
if not downloadable_list:
|
||||
continue
|
||||
if components:
|
||||
theme_name = self.format_name(theme_dir.name, "theme_packs")
|
||||
downloaded_themes[theme_name] = sorted(components)
|
||||
|
||||
if theme_name.lower() in {"none", "stock"}:
|
||||
continue
|
||||
downloaded_wheels = []
|
||||
for wheel_file in (THEME_SAVE_PATH / "steering_wheels").iterdir():
|
||||
if wheel_file.is_file():
|
||||
downloaded_wheels.append(self.format_name(wheel_file.name, "steering_wheels"))
|
||||
|
||||
if theme_component == "steering_wheels":
|
||||
theme_path = THEME_SAVE_PATH / "steering_wheels" / theme_name
|
||||
matching_files = list(theme_path.parent.glob(f"{theme_name}.*"))
|
||||
if not matching_files:
|
||||
print(f" {theme_name} for {theme_component} not found. Downloading...")
|
||||
self.download_theme(theme_component, theme_name, theme_param)
|
||||
update_frogpilot_toggles()
|
||||
elif theme_name.replace("_", " ").split(".")[0].title() not in downloadable_list:
|
||||
if theme_path.exists():
|
||||
print(f"{theme_name} for {theme_component} is outdated. Deleting...")
|
||||
delete_file(theme_path)
|
||||
continue
|
||||
else:
|
||||
theme_path = THEME_SAVE_PATH / "theme_packs" / theme_name / theme_component
|
||||
if not theme_path.exists():
|
||||
print(f" {theme_name} for {theme_component} not found. Downloading...")
|
||||
self.download_theme(theme_component, theme_name, theme_param)
|
||||
update_frogpilot_toggles()
|
||||
elif theme_name.replace("_", " ").split(".")[0].title() not in downloadable_list:
|
||||
if theme_path.exists():
|
||||
print(f"{theme_name} for {theme_component} is outdated. Deleting...")
|
||||
delete_file(theme_path)
|
||||
continue
|
||||
params.put("ThemesDownloaded", json.dumps({
|
||||
"themes": {key: downloaded_themes[key] for key in sorted(downloaded_themes)},
|
||||
"steering_wheels": sorted(downloaded_wheels)
|
||||
}))
|
||||
|
||||
for dir_path in THEME_SAVE_PATH.glob("**/*"):
|
||||
if dir_path.is_dir() and not any(dir_path.iterdir()):
|
||||
print(f"Deleting empty folder: {dir_path}")
|
||||
dir_path.rmdir()
|
||||
elif dir_path.is_file() and dir_path.name.startswith("tmp"):
|
||||
print(f"Deleting temp file: {dir_path}")
|
||||
dir_path.unlink()
|
||||
print("ThemesDownloaded updated successfully")
|
||||
|
||||
print("Theme validation complete.")
|
||||
def update_theme_size(self, theme_component, theme_name, file_size):
|
||||
if theme_component == "steering_wheels":
|
||||
key = "wheels"
|
||||
else:
|
||||
key = "themes"
|
||||
|
||||
if key not in self.theme_sizes:
|
||||
self.theme_sizes[key] = {}
|
||||
|
||||
if key == "wheels":
|
||||
self.theme_sizes[key][theme_name] = file_size
|
||||
else:
|
||||
if theme_name not in self.theme_sizes[key]:
|
||||
self.theme_sizes[key][theme_name] = {}
|
||||
self.theme_sizes[key][theme_name][theme_component] = file_size
|
||||
|
||||
update_json_file(self.theme_sizes_path, self.theme_sizes)
|
||||
|
||||
def update_themes(self, frogpilot_toggles, boot_run=False):
|
||||
if self.downloading_theme:
|
||||
@@ -498,7 +568,7 @@ class ThemeManager:
|
||||
print("GitHub and GitLab are offline...")
|
||||
return
|
||||
|
||||
assets = self.fetch_assets(repo_url)
|
||||
assets = self.fetch_assets(repo_url, frogpilot_toggles)
|
||||
if not assets:
|
||||
return
|
||||
|
||||
@@ -509,7 +579,7 @@ class ThemeManager:
|
||||
downloadable_sounds = []
|
||||
|
||||
for theme, available_assets in assets["themes"].items():
|
||||
theme_name = theme.replace("_", " ").split(".")[0].title()
|
||||
theme_name = self.format_name(theme, "theme_packs")
|
||||
print(f"Theme found: {theme_name}")
|
||||
|
||||
if "colors" in available_assets:
|
||||
@@ -523,7 +593,7 @@ class ThemeManager:
|
||||
if "sounds" in available_assets:
|
||||
downloadable_sounds.append(theme_name)
|
||||
|
||||
downloadable_wheels = [wheel.replace("_", " ").split(".")[0].title() for wheel in assets["wheels"]]
|
||||
downloadable_wheels = [self.format_name(wheel, "steering_wheels") for wheel in assets["wheels"]]
|
||||
|
||||
print(f"Downloadable Colors: {downloadable_colors}")
|
||||
print(f"Downloadable Icons: {downloadable_icons}")
|
||||
@@ -536,3 +606,67 @@ class ThemeManager:
|
||||
self.validate_themes(downloadable_colors, downloadable_distance_icons, downloadable_icons, downloadable_signals, downloadable_sounds, downloadable_wheels, frogpilot_toggles)
|
||||
|
||||
self.update_theme_params(downloadable_colors, downloadable_distance_icons, downloadable_icons, downloadable_signals, downloadable_sounds, downloadable_wheels)
|
||||
|
||||
def update_wheel_image(self, image, boot_run=False, random_event=False):
|
||||
wheel_save_location = ACTIVE_THEME_PATH / "steering_wheel"
|
||||
|
||||
if self.holiday_theme != "stock":
|
||||
wheel_location = HOLIDAY_THEME_PATH / self.holiday_theme / "steering_wheel"
|
||||
elif random_event:
|
||||
wheel_location = RANDOM_EVENTS_PATH / "steering_wheels"
|
||||
elif image == "stock":
|
||||
wheel_location = STOCKOP_THEME_PATH / "steering_wheel"
|
||||
elif image in HOLIDAY_SLUGS:
|
||||
wheel_location = HOLIDAY_THEME_PATH / image / "steering_wheel"
|
||||
elif f"{image}_week" in HOLIDAY_SLUGS:
|
||||
wheel_location = HOLIDAY_THEME_PATH / f"{image}_week" / "steering_wheel"
|
||||
else:
|
||||
wheel_location = THEME_SAVE_PATH / "steering_wheels"
|
||||
|
||||
if not wheel_location.exists():
|
||||
wheel_location = STOCKOP_THEME_PATH / "steering_wheel"
|
||||
print("Using the stock steering wheel instead")
|
||||
|
||||
delete_file(wheel_save_location, print_error=not boot_run)
|
||||
wheel_save_location.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
image_name = image.replace(" ", "_").lower()
|
||||
matching_files = [images for images in wheel_location.iterdir() if images.stem.lower() in {image_name, "wheel"}]
|
||||
if matching_files:
|
||||
source_file = matching_files[0]
|
||||
destination_file = wheel_save_location / f"wheel{source_file.suffix}"
|
||||
destination_file.symlink_to(source_file)
|
||||
print(f"Linked {destination_file} to {source_file}")
|
||||
|
||||
def validate_themes(self, downloadable_colors, downloadable_distance_icons, downloadable_icons, downloadable_signals, downloadable_sounds, downloadable_wheels, frogpilot_toggles):
|
||||
downloaded_data = json.loads(params.get("ThemesDownloaded") or "{}")
|
||||
|
||||
for display_name, components in downloaded_data.get("themes", {}).items():
|
||||
raw_name = display_name.lower().replace(" ", "_").replace("(", "").replace(")", "")
|
||||
theme_folder_name = raw_name.replace("_animated", "-animated")
|
||||
|
||||
for component in components:
|
||||
component_path = THEME_SAVE_PATH / "theme_packs" / theme_folder_name / component
|
||||
if not component_path.is_dir() or not any(component_path.iterdir()):
|
||||
print(f"Missing or empty component '{component}' for theme '{theme_folder_name}'. Downloading...")
|
||||
self.download_theme(component, theme_folder_name, THEME_COMPONENT_PARAMS.get(component), frogpilot_toggles)
|
||||
self.update_active_theme(True, frogpilot_toggles)
|
||||
|
||||
wheels_path = THEME_SAVE_PATH / "steering_wheels"
|
||||
for display_name in downloaded_data.get("steering_wheels", []):
|
||||
file_stem = display_name.replace(" ", "_").lower()
|
||||
matching_files = list(wheels_path.glob(f"{file_stem}.*"))
|
||||
if not matching_files:
|
||||
print(f"Missing steering wheel '{display_name}'. Downloading...")
|
||||
self.download_theme("steering_wheels", file_stem, THEME_COMPONENT_PARAMS["steering_wheels"], frogpilot_toggles)
|
||||
self.update_active_theme(True, frogpilot_toggles)
|
||||
|
||||
for dir_path in THEME_SAVE_PATH.glob("**/*"):
|
||||
if dir_path.is_dir() and not any(dir_path.iterdir()):
|
||||
print(f"Deleting empty folder: {dir_path}")
|
||||
delete_file(dir_path)
|
||||
elif dir_path.is_file() and dir_path.name.startswith("tmp"):
|
||||
print(f"Deleting temp file: {dir_path}")
|
||||
delete_file(dir_path)
|
||||
|
||||
print("Theme validation complete.")
|
||||
|
||||
@@ -172,7 +172,7 @@ def main(demo=False):
|
||||
cloudlog.warning(f"connected extra cam with buffer size: {vipc_client_extra.buffer_len} ({vipc_client_extra.width} x {vipc_client_extra.height})")
|
||||
|
||||
# messaging
|
||||
pm = PubMaster(["modelV2", "cameraOdometry"])
|
||||
pm = PubMaster(["modelV2", "cameraOdometry", "frogpilotModelV2"])
|
||||
sm = SubMaster(["deviceState", "carState", "roadCameraState", "liveCalibration", "driverMonitoringState", "navModel", "navInstruction", "carControl", "liveTracks", "liveDelay", "frogpilotPlan"])
|
||||
|
||||
publish_state = PublishState()
|
||||
@@ -316,6 +316,7 @@ def main(demo=False):
|
||||
|
||||
if model_output is not None:
|
||||
modelv2_send = messaging.new_message('modelV2')
|
||||
frogpilot_modelv2_send = messaging.new_message('frogpilotModelV2')
|
||||
posenet_send = messaging.new_message('cameraOdometry')
|
||||
fill_model_msg(modelv2_send, model_output, publish_state, meta_main.frame_id, meta_extra.frame_id, frame_id, frame_drop_ratio,
|
||||
meta_main.timestamp_eof, timestamp_llk, model_execution_time, live_calib_seen, nav_enabled)
|
||||
@@ -327,10 +328,11 @@ def main(demo=False):
|
||||
DH.update(sm['carState'], sm['carControl'].latActive, lane_change_prob, sm['frogpilotPlan'], frogpilot_toggles)
|
||||
modelv2_send.modelV2.meta.laneChangeState = DH.lane_change_state
|
||||
modelv2_send.modelV2.meta.laneChangeDirection = DH.lane_change_direction
|
||||
modelv2_send.modelV2.meta.turnDirection = DH.turn_direction
|
||||
frogpilot_modelv2_send.frogpilotModelV2.turnDirection = DH.turn_direction
|
||||
|
||||
fill_pose_msg(posenet_send, model_output, meta_main.frame_id, vipc_dropped_frames, meta_main.timestamp_eof, live_calib_seen)
|
||||
pm.send('modelV2', modelv2_send)
|
||||
pm.send('frogpilotModelV2', frogpilot_modelv2_send)
|
||||
pm.send('cameraOdometry', posenet_send)
|
||||
|
||||
last_vipc_frame_id = meta_main.frame_id
|
||||
|
||||
@@ -1,9 +1,8 @@
|
||||
#!/usr/bin/env python3
|
||||
from pathlib import Path
|
||||
|
||||
import datetime
|
||||
import filecmp
|
||||
import glob
|
||||
import json
|
||||
import os
|
||||
import random
|
||||
import shutil
|
||||
@@ -14,6 +13,8 @@ import threading
|
||||
import time
|
||||
import zstandard as zstd
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from openpilot.common.basedir import BASEDIR
|
||||
from openpilot.common.params import Params
|
||||
from openpilot.common.time import system_time_valid
|
||||
@@ -21,11 +22,11 @@ from openpilot.system.athena.registration import register
|
||||
from openpilot.system.hardware import HARDWARE
|
||||
|
||||
from openpilot.frogpilot.assets.model_manager import ModelManager
|
||||
from openpilot.frogpilot.assets.theme_manager import HOLIDAY_THEME_PATH, ThemeManager
|
||||
from openpilot.frogpilot.assets.theme_manager import ThemeManager
|
||||
from openpilot.frogpilot.common.frogpilot_utilities import delete_file, run_cmd, use_konik_server
|
||||
from openpilot.frogpilot.common.frogpilot_variables import (
|
||||
ERROR_LOGS_PATH, EXCLUDED_KEYS, HD_LOGS_PATH, KONIK_LOGS_PATH, MODELS_PATH, SCREEN_RECORDINGS_PATH,
|
||||
THEME_SAVE_PATH, FrogPilotVariables, frogpilot_default_params, get_frogpilot_toggles, params
|
||||
THEME_SAVE_PATH, VIDEO_CACHE_PATH, FrogPilotVariables, frogpilot_default_params, get_frogpilot_toggles, params
|
||||
)
|
||||
from openpilot.frogpilot.system.frogpilot_stats import send_stats
|
||||
|
||||
@@ -130,15 +131,31 @@ def backup_toggles(params_cache):
|
||||
def convert_params(params_cache):
|
||||
print("Starting to convert params")
|
||||
|
||||
if Path("/cache/tracking").exists():
|
||||
params_tracking = Params("/cache/tracking")
|
||||
|
||||
frogpilot_stats = json.loads(params.get("FrogPilotStats") or "{}")
|
||||
frogpilot_stats["FrogPilotDrives"] = params_tracking.get_int("FrogPilotDrives")
|
||||
frogpilot_stats["FrogPilotMeters"] = params_tracking.get_float("FrogPilotKilometers") * 1000
|
||||
frogpilot_stats["FrogPilotSeconds"] = params_tracking.get_float("FrogPilotMinutes") * 60
|
||||
|
||||
params.put("FrogPilotStats", json.dumps(frogpilot_stats))
|
||||
|
||||
delete_file("/cache/tracking")
|
||||
|
||||
print("Param conversion completed")
|
||||
|
||||
def frogpilot_boot_functions(build_metadata, params_cache):
|
||||
if params.get_bool("HasAcceptedTerms"):
|
||||
params_cache.clear_all()
|
||||
|
||||
FrogPilotVariables().update(holiday_theme="stock", started=False, boot_run=True)
|
||||
ModelManager().copy_default_model()
|
||||
ThemeManager().update_active_theme(time_validated=system_time_valid(), frogpilot_toggles=get_frogpilot_toggles(), boot_run=True)
|
||||
FrogPilotVariables().update(holiday_theme="stock", started=False)
|
||||
ModelManager(boot_run=True)
|
||||
ThemeManager(boot_run=True).update_active_theme(time_validated=system_time_valid(), frogpilot_toggles=get_frogpilot_toggles(), boot_run=True)
|
||||
|
||||
if VIDEO_CACHE_PATH.exists():
|
||||
for video in VIDEO_CACHE_PATH.glob("*.mp4"):
|
||||
delete_file(video)
|
||||
|
||||
if use_konik_server():
|
||||
if params.get("KonikDongleId", encoding="utf8") != None:
|
||||
@@ -172,26 +189,6 @@ def setup_frogpilot(build_metadata):
|
||||
SCREEN_RECORDINGS_PATH.mkdir(parents=True, exist_ok=True)
|
||||
THEME_SAVE_PATH.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
for source_suffix, destination_suffix in [
|
||||
("world_frog_day/colors", "theme_packs/frog/colors"),
|
||||
("world_frog_day/distance_icons", "theme_packs/frog-animated/distance_icons"),
|
||||
("world_frog_day/icons", "theme_packs/frog-animated/icons"),
|
||||
("world_frog_day/signals", "theme_packs/frog/signals"),
|
||||
("world_frog_day/sounds", "theme_packs/frog/sounds"),
|
||||
]:
|
||||
source = Path(HOLIDAY_THEME_PATH) / source_suffix
|
||||
destination = THEME_SAVE_PATH / destination_suffix
|
||||
destination.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copytree(source, destination, dirs_exist_ok=True)
|
||||
|
||||
for source_suffix, destination_suffix in [
|
||||
("world_frog_day/steering_wheel/wheel.png", "steering_wheels/frog.png"),
|
||||
]:
|
||||
source = Path(HOLIDAY_THEME_PATH) / source_suffix
|
||||
destination = THEME_SAVE_PATH / destination_suffix
|
||||
destination.parent.mkdir(parents=True, exist_ok=True)
|
||||
shutil.copy2(source, destination)
|
||||
|
||||
boot_logo_location = Path("/usr/comma/bg.jpg")
|
||||
frogpilot_boot_logo = Path(__file__).parents[1] / "assets/other_images/frogpilot_boot_logo.png"
|
||||
if not filecmp.cmp(frogpilot_boot_logo, boot_logo_location, shallow=False):
|
||||
|
||||
@@ -5,17 +5,18 @@ import numpy as np
|
||||
import requests
|
||||
import shutil
|
||||
import subprocess
|
||||
import tarfile
|
||||
import threading
|
||||
import time
|
||||
import urllib.error
|
||||
import urllib.request
|
||||
import zipfile
|
||||
|
||||
import openpilot.system.sentry as sentry
|
||||
|
||||
from functools import cache
|
||||
from pathlib import Path
|
||||
|
||||
import openpilot.system.sentry as sentry
|
||||
|
||||
from cereal import log, messaging
|
||||
from opendbc.can.parser import CANParser
|
||||
from openpilot.common.realtime import DT_DMON, DT_HW
|
||||
@@ -37,6 +38,7 @@ locks = {
|
||||
"update_checks": threading.Lock(),
|
||||
"update_maps": threading.Lock(),
|
||||
"update_openpilot": threading.Lock(),
|
||||
"update_tinygrad": threading.Lock()
|
||||
}
|
||||
|
||||
def run_thread_with_lock(name, target, args=(), report=True):
|
||||
@@ -99,20 +101,44 @@ def calculate_lane_width(lane, current_lane, road_edge=None):
|
||||
def calculate_road_curvature(modelData, v_ego):
|
||||
orientation_rate = np.array(modelData.orientationRate.z)
|
||||
velocity = np.array(modelData.velocity.x)
|
||||
timebase = np.array(modelData.orientationRate.t)
|
||||
|
||||
max_pred_lat_acc = max(np.max(orientation_rate * velocity), np.min(orientation_rate * velocity), key=abs)
|
||||
lateral_acceleration = orientation_rate * velocity
|
||||
index = np.argmax(np.abs(lateral_acceleration))
|
||||
predicted_lateral_acc = float(lateral_acceleration[index])
|
||||
time_to_curve = float(timebase[index])
|
||||
|
||||
return float(max_pred_lat_acc / max(v_ego, 1)**2)
|
||||
return predicted_lateral_acc / max(v_ego, 1)**2, max(time_to_curve, 1)
|
||||
|
||||
def delete_file(path, report=True):
|
||||
def clean_model_name(name):
|
||||
return (
|
||||
name.replace("🗺️", "")
|
||||
.replace("📡", "")
|
||||
.replace("👀", "")
|
||||
.replace("(Default)", "")
|
||||
.strip()
|
||||
)
|
||||
|
||||
def delete_file(path, print_error=True, report=True):
|
||||
path = Path(path)
|
||||
if path.is_file() or path.is_symlink():
|
||||
run_cmd(["sudo", "rm", "-f", str(path)], success_message=f"Deleted file: {path}", fail_message=f"Failed to delete file: {path}", report=report)
|
||||
run_cmd(["sudo", "rm", "-f", str(path)], f"Deleted file: {path}", f"Failed to delete file: {path}", report=report)
|
||||
elif path.is_dir():
|
||||
run_cmd(["sudo", "rm", "-rf", str(path)], success_message=f"Deleted directory: {path}", fail_message=f"Failed to delete directory: {path}", report=report)
|
||||
else:
|
||||
run_cmd(["sudo", "rm", "-rf", str(path)], f"Deleted directory: {path}", f"Failed to delete directory: {path}", report=report)
|
||||
elif print_error:
|
||||
print(f"File not found: {path}")
|
||||
|
||||
def extract_tar(tar_file, extract_path):
|
||||
tar_file = Path(tar_file)
|
||||
extract_path = Path(extract_path)
|
||||
print(f"Extracting {tar_file} to {extract_path}")
|
||||
|
||||
with tarfile.open(tar_file, "r:gz") as tar:
|
||||
tar.extractall(path=extract_path)
|
||||
|
||||
tar_file.unlink()
|
||||
print(f"Extraction completed: {tar_file} has been removed")
|
||||
|
||||
def extract_zip(zip_file, extract_path):
|
||||
zip_file = Path(zip_file)
|
||||
extract_path = Path(extract_path)
|
||||
@@ -146,16 +172,22 @@ def is_url_pingable(url):
|
||||
headers = {"User-Agent": "frogpilot-ping-test/1.0 (https://github.com/FrogAi/FrogPilot)"}
|
||||
try:
|
||||
response = requests.head(url, headers=headers, timeout=10, allow_redirects=True)
|
||||
response.raise_for_status()
|
||||
return True
|
||||
except requests.RequestException as exception:
|
||||
print(f"Network/HTTP error for {url}: {exception}")
|
||||
if response.status_code in (405, 501):
|
||||
response = requests.get(url, headers=headers, timeout=10, allow_redirects=True, stream=True)
|
||||
return response.ok
|
||||
except requests.exceptions.RequestException as error:
|
||||
print(f"{error.__class__.__name__} while pinging {url}: {error}")
|
||||
return False
|
||||
except Exception as exception:
|
||||
print(f"An unexpected error occurred while checking {url}: {exception}")
|
||||
sentry.capture_exception(exception)
|
||||
print(f"Unexpected error while pinging {url}: {exception}")
|
||||
return False
|
||||
|
||||
def load_json_file(path):
|
||||
if path.is_file():
|
||||
with open(path) as file:
|
||||
return json.load(file)
|
||||
return {}
|
||||
|
||||
def lock_doors(lock_doors_timer, sm):
|
||||
wait_for_no_driver(sm, door_checks=True, time_threshold=lock_doors_timer)
|
||||
|
||||
@@ -180,19 +212,26 @@ def lock_doors(lock_doors_timer, sm):
|
||||
|
||||
def run_cmd(cmd, success_message, fail_message, report=True, env=None):
|
||||
try:
|
||||
subprocess.run(cmd, capture_output=True, check=True, env=env, text=True)
|
||||
result = subprocess.run(cmd, capture_output=True, check=True, env=env, text=True)
|
||||
print(success_message)
|
||||
return result.stdout.strip()
|
||||
except subprocess.CalledProcessError as error:
|
||||
print(f"Command failed with return code {error.returncode}")
|
||||
if error.stderr:
|
||||
print(f"Error Output: {error.stderr.strip()}")
|
||||
if report:
|
||||
sentry.capture_exception(error)
|
||||
return None
|
||||
except Exception as exception:
|
||||
print(f"Unexpected error occurred: {exception}")
|
||||
print(fail_message)
|
||||
if report:
|
||||
sentry.capture_exception(exception)
|
||||
return None
|
||||
|
||||
def update_json_file(path, data):
|
||||
with open(path, "w") as file:
|
||||
json.dump(data, file, indent=2, sort_keys=True)
|
||||
|
||||
def update_maps(now):
|
||||
while not MAPD_PATH.exists():
|
||||
@@ -233,7 +272,7 @@ def update_maps(now):
|
||||
|
||||
def update_openpilot():
|
||||
def update_available():
|
||||
subprocess.run(["pkill", "-SIGUSR1", "-f", "system.updated.updated"], check=False)
|
||||
run_cmd(["pkill", "-SIGUSR1", "-f", "system.updated.updated"], "Updater check signal sent", "Failed to send updater check signal", report=False)
|
||||
|
||||
while params.get("UpdaterState", encoding="utf-8") != "checking...":
|
||||
time.sleep(1)
|
||||
@@ -247,25 +286,22 @@ def update_openpilot():
|
||||
while params.get("UpdaterState", encoding="utf-8") != "idle":
|
||||
time.sleep(60)
|
||||
|
||||
subprocess.run(["pkill", "-SIGHUP", "-f", "system.updated.updated"], check=False)
|
||||
run_cmd(["pkill", "-SIGHUP", "-f", "system.updated.updated"], "Updater refresh signal sent", "Failed to send updater refresh signal", report=False)
|
||||
|
||||
while not params.get_bool("UpdateAvailable"):
|
||||
time.sleep(60)
|
||||
|
||||
return True
|
||||
|
||||
while params_memory.get_bool("DownloadAllModels") or params_memory.get("ModelToDownload") or params_memory.get_bool("UpdateSpeedLimits"):
|
||||
time.sleep(60)
|
||||
|
||||
if params.get("UpdaterState", encoding="utf-8") != "idle":
|
||||
return
|
||||
|
||||
while params.get_bool("IsOnroad") or params_memory.get_bool("UpdateSpeedLimits") or running_threads.get("lock_doors", threading.Thread()).is_alive():
|
||||
time.sleep(60)
|
||||
|
||||
if not update_available():
|
||||
return
|
||||
|
||||
while running_threads.get("lock_doors", threading.Thread()).is_alive() or params_memory.get_bool("IsOnroad"):
|
||||
time.sleep(60)
|
||||
|
||||
while True:
|
||||
if not update_available():
|
||||
break
|
||||
|
||||
@@ -23,20 +23,21 @@ from openpilot.selfdrive.modeld.constants import ModelConstants
|
||||
from openpilot.system.hardware import HARDWARE
|
||||
from openpilot.system.hardware.power_monitoring import VBATT_PAUSE_CHARGING
|
||||
from openpilot.system.version import get_build_metadata
|
||||
from panda import ALTERNATIVE_EXPERIENCE, Panda
|
||||
from panda import ALTERNATIVE_EXPERIENCE
|
||||
|
||||
params = Params()
|
||||
params_cache = Params("/cache/params")
|
||||
params_default = Params("/dev/shm/params_default")
|
||||
params_memory = Params("/dev/shm/params")
|
||||
params_tracking = Params("/cache/tracking")
|
||||
|
||||
GearShifter = car.CarState.GearShifter
|
||||
SafetyModel = car.CarParams.SafetyModel
|
||||
|
||||
CITY_SPEED_LIMIT = 25 # 55mph is typically the minimum speed for highways
|
||||
CRUISING_SPEED = 5 # Roughly the speed cars go when not touching the gas while in drive
|
||||
DEFAULT_LATERAL_ACCELERATION = 2.0 # m/s^2, typical lateral acceleration when taking curves
|
||||
EARTH_RADIUS = 6378137 # Radius of the Earth in meters
|
||||
MAX_T_FOLLOW = 3.0 # Maximum allowed following duration. Larger values risk losing track of the lead but may be increased as models improve
|
||||
MINIMUM_LATERAL_ACCELERATION = 1.3 # m/s^2, typical minimum lateral acceleration when taking curves
|
||||
PLANNER_TIME = ModelConstants.T_IDXS[-1] # Length of time the model projects out for
|
||||
THRESHOLD = 0.63 # Requires the condition to be true for ~1 second
|
||||
@@ -53,6 +54,7 @@ THEME_SAVE_PATH = Path("/data/themes")
|
||||
|
||||
ERROR_LOGS_PATH = Path("/data/error_logs")
|
||||
SCREEN_RECORDINGS_PATH = Path("/data/media/screen_recordings")
|
||||
VIDEO_CACHE_PATH = Path("/data/video_cache")
|
||||
|
||||
BACKUP_PATH = Path("/cache/on_backup")
|
||||
|
||||
@@ -65,20 +67,11 @@ KONIK_PATH = Path("/cache/use_konik")
|
||||
MAPD_PATH = Path("/data/media/0/osm/mapd")
|
||||
MAPS_PATH = Path("/data/media/0/osm/offline")
|
||||
|
||||
NEURAL_PARAMS_PATH = Path(BASEDIR) / "selfdrive/car/torque_data/neural_ff_weights.json"
|
||||
TORQUE_NN_MODEL_PATH = Path(BASEDIR) / "frogpilot/assets/nnff_models"
|
||||
NNFF_MODELS_PATH = Path(BASEDIR) / "frogpilot/assets/nnff_models"
|
||||
|
||||
DEFAULT_CLASSIC_MODEL = "wd-40"
|
||||
DEFAULT_CLASSIC_MODEL_NAME = "WD-40 (Default) 👀📡"
|
||||
DEFAULT_CLASSIC_MODEL_VERSION = "v2"
|
||||
|
||||
DEFAULT_MODEL = "national-public-radio"
|
||||
DEFAULT_MODEL_NAME = "National Public Radio 👀📡"
|
||||
DEFAULT_MODEL_VERSION = "v6"
|
||||
|
||||
DEFAULT_TINYGRAD_MODEL = "space-lab"
|
||||
DEFAULT_TINYGRAD_MODEL_NAME = "Space Lab 👀📡"
|
||||
DEFAULT_TINYGRAD_MODEL_VERSION = "v7"
|
||||
DEFAULT_MODEL = "firehose"
|
||||
DEFAULT_MODEL_NAME = "Firehose (Default) 👀📡"
|
||||
DEFAULT_MODEL_VERSION = "v9"
|
||||
|
||||
BUTTON_FUNCTIONS = {
|
||||
"NOTHING": 0,
|
||||
@@ -91,9 +84,9 @@ BUTTON_FUNCTIONS = {
|
||||
}
|
||||
|
||||
EXCLUDED_KEYS = {
|
||||
"AvailableModels", "AvailableModelNames", "CarParamsPersistent",
|
||||
"ExperimentalLongitudinalEnabled", "KonikMinutes", "MapBoxRequests", "ModelDrivesAndScores",
|
||||
"ModelVersions", "openpilotMinutes", "OverpassRequests", "SpeedLimits", "SpeedLimitsFiltered", "UpdaterAvailableBranches"
|
||||
"AvailableModels", "AvailableModelNames", "CalibratedLateralAcceleration", "CalibrationProgress", "CarParamsPersistent",
|
||||
"CurvatureData", "ExperimentalLongitudinalEnabled", "KonikMinutes", "MapBoxRequests", "ModelDrivesAndScores", "ModelVersions",
|
||||
"openpilotMinutes", "OverpassRequests", "SpeedLimits", "SpeedLimitsFiltered", "UpdaterAvailableBranches"
|
||||
}
|
||||
|
||||
TINYGRAD_FILES = [
|
||||
@@ -103,17 +96,9 @@ TINYGRAD_FILES = [
|
||||
("driving_vision_tinygrad.pkl", "vision model"),
|
||||
]
|
||||
|
||||
@cache
|
||||
def get_comma_nnff_model_file():
|
||||
with open(NEURAL_PARAMS_PATH, "r") as file:
|
||||
return json.load(file)
|
||||
|
||||
def comma_nnff_supported(car):
|
||||
return car in get_comma_nnff_model_file()
|
||||
|
||||
@cache
|
||||
def get_nnff_model_files():
|
||||
model_dir = Path(TORQUE_NN_MODEL_PATH)
|
||||
model_dir = Path(NNFF_MODELS_PATH)
|
||||
return [file.stem for file in model_dir.iterdir() if file.is_file()]
|
||||
|
||||
def nnff_supported(car_fingerprint):
|
||||
@@ -123,8 +108,8 @@ def nnff_supported(car_fingerprint):
|
||||
|
||||
return False
|
||||
|
||||
def get_frogpilot_toggles(block=True):
|
||||
return SimpleNamespace(**json.loads(params_memory.get("FrogPilotToggles", block=block) or "{}"))
|
||||
def get_frogpilot_toggles():
|
||||
return SimpleNamespace(**json.loads(params_memory.get("FrogPilotToggles") or "{}"))
|
||||
|
||||
def update_frogpilot_toggles():
|
||||
params_memory.put_bool("FrogPilotTogglesUpdated", True)
|
||||
@@ -132,11 +117,11 @@ def update_frogpilot_toggles():
|
||||
frogpilot_default_params: list[tuple[str, str | bytes, int, str]] = [
|
||||
("AccelerationPath", "1", 2, "0"),
|
||||
("AccelerationProfile", "2", 0, "0"),
|
||||
("AdjacentLeadsUI", "0", 3, "0"),
|
||||
("AdjacentLeadsUI", "1", 3, "0"),
|
||||
("AdjacentPath", "0", 3, "0"),
|
||||
("AdjacentPathMetrics", "0", 3, "0"),
|
||||
("AdvancedCustomUI", "0", 2, "0"),
|
||||
("AdvancedLateralTune", "0", 2, "0"),
|
||||
("AdvancedLateralTune", "0", 3, "0"),
|
||||
("AdvancedLongitudinalTune", "0", 3, "0"),
|
||||
("AggressiveFollow", "1.25", 2, "1.25"),
|
||||
("AggressiveJerkAcceleration", "50", 3, "50"),
|
||||
@@ -148,8 +133,8 @@ frogpilot_default_params: list[tuple[str, str | bytes, int, str]] = [
|
||||
("AlertVolumeControl", "0", 2, "0"),
|
||||
("AlwaysOnDM", "0", 0, "0"),
|
||||
("AlwaysOnLateral", "1", 0, "0"),
|
||||
("AlwaysOnLateralLKAS", "1", 0, "0"),
|
||||
("AlwaysOnLateralMain", "1", 0, "0"),
|
||||
("AlwaysOnLateralLKAS", "1", 2, "0"),
|
||||
("AlwaysOnLateralMain", "1", 2, "0"),
|
||||
("AMapKey1", "", 0, ""),
|
||||
("AMapKey2", "", 0, ""),
|
||||
("AutomaticallyDownloadModels", "1", 1, "0"),
|
||||
@@ -161,6 +146,8 @@ frogpilot_default_params: list[tuple[str, str | bytes, int, str]] = [
|
||||
("BlindSpotMetrics", "1", 3, "0"),
|
||||
("BlindSpotPath", "1", 1, "0"),
|
||||
("BorderMetrics", "0", 3, "0"),
|
||||
("CalibratedLateralAcceleration", str(DEFAULT_LATERAL_ACCELERATION), 2, str(DEFAULT_LATERAL_ACCELERATION)),
|
||||
("CalibrationProgress", "0", 3, "0"),
|
||||
("CameraView", "3", 2, "0"),
|
||||
("CarMake", "", 0, ""),
|
||||
("CarModel", "", 0, ""),
|
||||
@@ -171,7 +158,7 @@ frogpilot_default_params: list[tuple[str, str | bytes, int, str]] = [
|
||||
("CELead", "0", 1, "0"),
|
||||
("CEModelStopTime", str(PLANNER_TIME - 2), 2, "0"),
|
||||
("CENavigation", "1", 2, "0"),
|
||||
("CENavigationIntersections", "1", 2, "0"),
|
||||
("CENavigationIntersections", "0", 2, "0"),
|
||||
("CENavigationLead", "1", 2, "0"),
|
||||
("CENavigationTurns", "1", 2, "0"),
|
||||
("CESignalSpeed", "55", 2, "0"),
|
||||
@@ -182,10 +169,10 @@ frogpilot_default_params: list[tuple[str, str | bytes, int, str]] = [
|
||||
("CEStoppedLead", "0", 1, "0"),
|
||||
("ClusterOffset", "1.015", 2, "1.015"),
|
||||
("Compass", "0", 1, "0"),
|
||||
("ConditionalExperimental", "1", 0, "0"),
|
||||
("CurveSensitivity", "100", 2, "100"),
|
||||
("CurveSpeedControl", "1", 1, "0"),
|
||||
("CustomAlerts", "1", 0, "0"),
|
||||
("ConditionalExperimental", "1", 1, "0"),
|
||||
("CurvatureData", "", 2, ""),
|
||||
("CurveSpeedController", "1", 1, "0"),
|
||||
("CustomAlerts", "0", 0, "0"),
|
||||
("CustomColors", "frog", 0, "stock"),
|
||||
("CustomCruise", "1", 2, "1"),
|
||||
("CustomCruiseLong", "5", 2, "5"),
|
||||
@@ -216,19 +203,19 @@ frogpilot_default_params: list[tuple[str, str | bytes, int, str]] = [
|
||||
("DistanceButtonControl", "1", 2, "0"),
|
||||
("DriverCamera", "0", 1, "0"),
|
||||
("DynamicPathWidth", "0", 2, "0"),
|
||||
("DynamicPedalsOnUI", "1", 2, "0"),
|
||||
("DynamicPedalsOnUI", "1", 1, "0"),
|
||||
("EngageVolume", "101", 2, "101"),
|
||||
("ExperimentalGMTune", "0", 2, "0"),
|
||||
("ExperimentalLongitudinalEnabled", "0", 0, "0"),
|
||||
("ExperimentalModeConfirmed", "0", 0, "0"),
|
||||
("Fahrenheit", "0", 3, "0"),
|
||||
("FavoriteDestinations", "", 0, ""),
|
||||
("ForceAutoTune", "0", 2, "0"),
|
||||
("ForceAutoTuneOff", "0", 2, "0"),
|
||||
("ForceAutoTune", "0", 3, "0"),
|
||||
("ForceAutoTuneOff", "0", 3, "0"),
|
||||
("ForceFingerprint", "0", 2, "0"),
|
||||
("ForceMPHDashboard", "0", 2, "0"),
|
||||
("ForceStandstill", "0", 2, "0"),
|
||||
("ForceMPHDashboard", "0", 3, "0"),
|
||||
("ForceStops", "0", 2, "0"),
|
||||
("ForceTorqueController", "0", 3, "0"),
|
||||
("FPSCounter", "1", 3, "0"),
|
||||
("FrogPilotDongleId", "", 0, ""),
|
||||
("FrogPilotStats", "", 0, ""),
|
||||
@@ -243,11 +230,12 @@ frogpilot_default_params: list[tuple[str, str | bytes, int, str]] = [
|
||||
("GsmRoaming", "1", 0, "0"),
|
||||
("HideAlerts", "0", 2, "0"),
|
||||
("HideLeadMarker", "0", 2, "0"),
|
||||
("HideMap", "0", 2, "0"),
|
||||
("HideMapIcon", "0", 2, "0"),
|
||||
("HideMaxSpeed", "0", 2, "0"),
|
||||
("HideSpeed", "0", 2, "0"),
|
||||
("HideSpeedLimit", "0", 2, "0"),
|
||||
("HigherBitrate", "0", 3, "0"),
|
||||
("HigherBitrate", "0", 2, "0"),
|
||||
("HolidayThemes", "1", 0, "0"),
|
||||
("HumanAcceleration", "1", 2, "0"),
|
||||
("HumanFollowing", "1", 2, "0"),
|
||||
@@ -257,14 +245,14 @@ frogpilot_default_params: list[tuple[str, str | bytes, int, str]] = [
|
||||
("IsMetric", "0", 0, "0"),
|
||||
("KonikDongleId", "", 0, ""),
|
||||
("KonikMinutes", "0", 0, "0"),
|
||||
("LaneChangeCustomizations", "1", 0, "1"),
|
||||
("LaneChangeTime", "2.0", 0, "0"),
|
||||
("LaneDetectionWidth", "0", 2, "0"),
|
||||
("LaneChanges", "1", 0, "1"),
|
||||
("LaneChangeTime", "1.0", 1, "0"),
|
||||
("LaneDetectionWidth", "0", 1, "0"),
|
||||
("LaneLinesWidth", "4", 2, "2"),
|
||||
("LateralTune", "1", 2, "0"),
|
||||
("LateralTune", "1", 1, "0"),
|
||||
("LeadDepartingAlert", "0", 0, "0"),
|
||||
("LeadDetectionThreshold", "35", 3, "50"),
|
||||
("LeadInfo", "1", 2, "0"),
|
||||
("LeadInfo", "1", 3, "0"),
|
||||
("LiveDelay", "", 0, ""),
|
||||
("LKASButtonControl", "5", 2, "0"),
|
||||
("LockDoors", "1", 0, "0"),
|
||||
@@ -275,33 +263,31 @@ frogpilot_default_params: list[tuple[str, str | bytes, int, str]] = [
|
||||
("LongitudinalTune", "1", 0, "0"),
|
||||
("LongPitch", "1", 2, "0"),
|
||||
("LoudBlindspotAlert", "0", 0, "0"),
|
||||
("LowVoltageShutdown", str(VBATT_PAUSE_CHARGING), 2, str(VBATT_PAUSE_CHARGING)),
|
||||
("LowVoltageShutdown", str(VBATT_PAUSE_CHARGING), 3, str(VBATT_PAUSE_CHARGING)),
|
||||
("MapAcceleration", "0", 1, "0"),
|
||||
("MapboxPublicKey", "", 0, ""),
|
||||
("MapboxSecretKey", "", 0, ""),
|
||||
("MapDeceleration", "0", 1, "0"),
|
||||
("MapGears", "0", 1, "0"),
|
||||
("MapGears", "0", 2, "0"),
|
||||
("MapsSelected", "", 0, ""),
|
||||
("MapStyle", "1", 2, "0"),
|
||||
("MapTurnControl", "1", 1, "0"),
|
||||
("MaxDesiredAcceleration", "4.0", 3, "2.0"),
|
||||
("MaxDesiredAcceleration", "4.0", 2, "2.0"),
|
||||
("MinimumLaneChangeSpeed", str(LANE_CHANGE_SPEED_MIN / CV.MPH_TO_MS), 2, str(LANE_CHANGE_SPEED_MIN / CV.MPH_TO_MS)),
|
||||
("Model", DEFAULT_CLASSIC_MODEL, 1, DEFAULT_CLASSIC_MODEL),
|
||||
("Model", DEFAULT_MODEL + "_default", 1, DEFAULT_MODEL + "_default"),
|
||||
("ModelDrivesAndScores", "", 2, ""),
|
||||
("ModelRandomizer", "0", 2, "0"),
|
||||
("ModelUI", "1", 2, "0"),
|
||||
("ModelVersions", "", 2, ""),
|
||||
("MTSCCurvatureCheck", "1", 2, "1"),
|
||||
("NavigationUI", "1", 1, "0"),
|
||||
("NavSettingLeftSide", "0", 0, "0"),
|
||||
("NavSettingTime24h", "0", 0, "0"),
|
||||
("NewLongAPI", "1", 2, "1"),
|
||||
("NewLongAPI", "1", 3, "1"),
|
||||
("NNFF", "1", 2, "0"),
|
||||
("NNFFLite", "1", 2, "0"),
|
||||
("NoLogging", "0", 2, "0"),
|
||||
("NoUploads", "0", 2, "0"),
|
||||
("NudgelessLaneChange", "0", 0, "0"),
|
||||
("NumericalTemp", "1", 2, "0"),
|
||||
("NudgelessLaneChange", "1", 0, "0"),
|
||||
("NumericalTemp", "1", 3, "0"),
|
||||
("Offset1", "5", 0, "0"),
|
||||
("Offset2", "5", 0, "0"),
|
||||
("Offset3", "5", 0, "0"),
|
||||
@@ -314,15 +300,15 @@ frogpilot_default_params: list[tuple[str, str | bytes, int, str]] = [
|
||||
("openpilotMinutes", "0", 0, "0"),
|
||||
("PathEdgeWidth", "20", 2, "0"),
|
||||
("PathWidth", "6.1", 2, "5.9"),
|
||||
("PauseAOLOnBrake", "0", 2, "0"),
|
||||
("PauseLateralOnSignal", "0", 2, "0"),
|
||||
("PauseLateralSpeed", "0", 2, "0"),
|
||||
("PedalsOnUI", "0", 2, "0"),
|
||||
("PauseAOLOnBrake", "0", 1, "0"),
|
||||
("PauseLateralOnSignal", "0", 1, "0"),
|
||||
("PauseLateralSpeed", "0", 1, "0"),
|
||||
("PedalsOnUI", "0", 1, "0"),
|
||||
("PersonalizeOpenpilot", "1", 0, "0"),
|
||||
("PreferredSchedule", "2", 0, "0"),
|
||||
("PromptDistractedVolume", "101", 2, "101"),
|
||||
("PromptVolume", "101", 2, "101"),
|
||||
("QOLLateral", "1", 2, "0"),
|
||||
("QOLLateral", "1", 1, "0"),
|
||||
("QOLLongitudinal", "1", 1, "0"),
|
||||
("QOLVisuals", "1", 0, "0"),
|
||||
("RadarTracksUI", "0", 3, "0"),
|
||||
@@ -340,11 +326,11 @@ frogpilot_default_params: list[tuple[str, str | bytes, int, str]] = [
|
||||
("RelaxedPersonalityProfile", "1", 2, "0"),
|
||||
("ReverseCruise", "0", 1, "0"),
|
||||
("RoadEdgesWidth", "2", 2, "2"),
|
||||
("RoadNameUI", "1", 2, "0"),
|
||||
("RoadNameUI", "1", 1, "0"),
|
||||
("RotatingWheel", "1", 1, "0"),
|
||||
("ScreenBrightness", "101", 2, "101"),
|
||||
("ScreenBrightnessOnroad", "101", 2, "101"),
|
||||
("ScreenManagement", "1", 2, "0"),
|
||||
("ScreenManagement", "1", 1, "0"),
|
||||
("ScreenRecorder", "1", 2, "0"),
|
||||
("ScreenTimeout", "30", 2, "30"),
|
||||
("ScreenTimeoutOnroad", "30", 2, "10"),
|
||||
@@ -359,15 +345,16 @@ frogpilot_default_params: list[tuple[str, str | bytes, int, str]] = [
|
||||
("ShowGPU", "0", 3, "0"),
|
||||
("ShowIP", "0", 3, "0"),
|
||||
("ShowMemoryUsage", "1", 3, "0"),
|
||||
("ShownToggleDescriptions", "", 0, ""),
|
||||
("ShowSLCOffset", "1", 0, "0"),
|
||||
("ShowSpeedLimits", "1", 1, "0"),
|
||||
("ShowSteering", "0", 3, "0"),
|
||||
("ShowStoppingPoint", "0", 2, "0"),
|
||||
("ShowStoppingPointMetrics", "0", 2, "0"),
|
||||
("ShowStoppingPoint", "1", 3, "0"),
|
||||
("ShowStoppingPointMetrics", "1", 3, "0"),
|
||||
("ShowStorageLeft", "0", 3, "0"),
|
||||
("ShowStorageUsed", "0", 3, "0"),
|
||||
("Sidebar", "0", 0, "0"),
|
||||
("SignalMetrics", "0", 2, "0"),
|
||||
("SignalMetrics", "0", 3, "0"),
|
||||
("SLCConfirmation", "0", 0, "0"),
|
||||
("SLCConfirmationHigher", "0", 0, "0"),
|
||||
("SLCConfirmationLower", "0", 0, "0"),
|
||||
@@ -380,10 +367,10 @@ frogpilot_default_params: list[tuple[str, str | bytes, int, str]] = [
|
||||
("SLCPriority2", "Map Data", 2, "Map Data"),
|
||||
("SLCPriority3", "Dashboard", 2, "Dashboard"),
|
||||
("SNGHack", "1", 2, "0"),
|
||||
("SpeedLimitChangedAlert", "1", 0, "0"),
|
||||
("SpeedLimitChangedAlert", "0", 0, "0"),
|
||||
("SpeedLimitController", "1", 0, "0"),
|
||||
("SpeedLimitFiller", "0", 0, "0"),
|
||||
("SpeedLimitSources", "0", 2, "0"),
|
||||
("SpeedLimitSources", "0", 3, "0"),
|
||||
("SshEnabled", "0", 0, "0"),
|
||||
("StartupMessageBottom", "Human-tested, frog-approved 🐸", 0, "Always keep hands on wheel and eyes on road"),
|
||||
("StartupMessageTop", "Hop in and buckle up!", 0, "Be ready to take over at any time"),
|
||||
@@ -394,10 +381,10 @@ frogpilot_default_params: list[tuple[str, str | bytes, int, str]] = [
|
||||
("StandardJerkSpeed", "100", 3, "100"),
|
||||
("StandardJerkSpeedDecrease", "100", 3, "100"),
|
||||
("StandardPersonalityProfile", "1", 2, "0"),
|
||||
("StandbyMode", "0", 2, "0"),
|
||||
("StandbyMode", "0", 1, "0"),
|
||||
("StartAccel", "", 3, ""),
|
||||
("StartAccelStock", "", 3, ""),
|
||||
("StaticPedalsOnUI", "0", 2, "0"),
|
||||
("StaticPedalsOnUI", "0", 1, "0"),
|
||||
("SteerDelay", "", 3, ""),
|
||||
("SteerDelayStock", "", 3, ""),
|
||||
("SteerFriction", "", 3, ""),
|
||||
@@ -416,6 +403,8 @@ frogpilot_default_params: list[tuple[str, str | bytes, int, str]] = [
|
||||
("TacoTune", "0", 2, "0"),
|
||||
("TacoTuneHacks", "0", 2, "0"),
|
||||
("TetheringEnabled", "0", 0, "0"),
|
||||
("ThemesDownloaded", "", 0, ""),
|
||||
("TinygradUpdateAvailable", "0", 1, "0"),
|
||||
("ToyotaDoors", "1", 0, "0"),
|
||||
("TrafficFollow", "0.5", 2, "0.5"),
|
||||
("TrafficJerkAcceleration", "50", 3, "50"),
|
||||
@@ -426,7 +415,6 @@ frogpilot_default_params: list[tuple[str, str | bytes, int, str]] = [
|
||||
("TrafficPersonalityProfile", "1", 2, "0"),
|
||||
("TuningLevel", "0", 0, "0"),
|
||||
("TuningLevelConfirmed", "0", 0, "0"),
|
||||
("TurnAggressiveness", "100", 2, "100"),
|
||||
("TurnDesires", "0", 2, "0"),
|
||||
("UnlimitedLength", "1", 2, "0"),
|
||||
("UnlockDoors", "1", 0, "0"),
|
||||
@@ -439,7 +427,6 @@ frogpilot_default_params: list[tuple[str, str | bytes, int, str]] = [
|
||||
("VEgoStopping", "", 3, ""),
|
||||
("VEgoStoppingStock", "", 3, ""),
|
||||
("VeryLongDistanceButtonControl", "6", 2, "0"),
|
||||
("VisionTurnControl", "1", 1, "0"),
|
||||
("VoltSNG", "0", 2, "0"),
|
||||
("WarningImmediateVolume", "101", 2, "101"),
|
||||
("WarningSoftVolume", "101", 2, "101"),
|
||||
@@ -455,7 +442,7 @@ misc_tuning_levels: list[tuple[str, str | bytes, int, str]] = [
|
||||
|
||||
class FrogPilotVariables:
|
||||
def __init__(self):
|
||||
self.frogpilot_toggles = get_frogpilot_toggles(block=False)
|
||||
self.frogpilot_toggles = get_frogpilot_toggles()
|
||||
self.tuning_levels = {key: lvl for key, _, lvl, _ in frogpilot_default_params + misc_tuning_levels}
|
||||
|
||||
short_branch = get_build_metadata().channel
|
||||
@@ -510,7 +497,7 @@ class FrogPilotVariables:
|
||||
|
||||
params_memory.put("FrogPilotTuningLevels", json.dumps(self.tuning_levels))
|
||||
|
||||
def update(self, holiday_theme, started, boot_run=False):
|
||||
def update(self, holiday_theme, started):
|
||||
default = params_default
|
||||
level = self.tuning_levels
|
||||
toggle = self.frogpilot_toggles
|
||||
@@ -532,7 +519,7 @@ class FrogPilotVariables:
|
||||
CP = cp_reader.as_builder()
|
||||
else:
|
||||
CarInterface, _, _ = interfaces[MOCK.MOCK]
|
||||
CP = CarInterface.get_params(MOCK.MOCK, gen_empty_fingerprint(), [], False, toggle, params, False)
|
||||
CP = CarInterface.get_params(MOCK.MOCK, gen_empty_fingerprint(), [], False, toggle, False)
|
||||
CarInterface.configure_torque_tune(MOCK.MOCK, CP.lateralTuning)
|
||||
|
||||
safety_config = car.CarParams.SafetyConfig.new_message()
|
||||
@@ -545,36 +532,35 @@ class FrogPilotVariables:
|
||||
FPCP = fpcp_reader.as_builder()
|
||||
else:
|
||||
CarInterface, _, _ = interfaces[MOCK.MOCK]
|
||||
FPCP = CarInterface.get_frogpilot_params(MOCK.MOCK, gen_empty_fingerprint(), [], toggle)
|
||||
FPCP = CarInterface.get_frogpilot_params(MOCK.MOCK, gen_empty_fingerprint(), [], CP, toggle)
|
||||
|
||||
is_torque_car = CP.lateralTuning.which() == "torque"
|
||||
is_torque_car = FPCP.lateralTuning.which() == "torque"
|
||||
if not is_torque_car:
|
||||
CarInterfaceBase.configure_torque_tune("MOCK", CP.lateralTuning)
|
||||
CarInterfaceBase.configure_torque_tune(MOCK.MOCK, FPCP.lateralTuning)
|
||||
|
||||
always_on_lateral_set = bool(CP.alternativeExperience & ALTERNATIVE_EXPERIENCE.ALWAYS_ON_LATERAL)
|
||||
toggle.always_on_lateral_set = bool(CP.alternativeExperience & ALTERNATIVE_EXPERIENCE.ALWAYS_ON_LATERAL)
|
||||
toggle.car_make = CP.carName
|
||||
toggle.car_model = CP.carFingerprint
|
||||
toggle.disable_openpilot_long = params.get_bool("DisableOpenpilotLongitudinal") if tuning_level >= level["DisableOpenpilotLongitudinal"] else default.get_bool("DisableOpenpilotLongitudinal")
|
||||
friction = CP.lateralTuning.torque.friction
|
||||
has_auto_tune = toggle.car_make in {"hyundai", "toyota"} and CP.lateralTuning.which() == "torque"
|
||||
friction = FPCP.lateralTuning.torque.friction
|
||||
has_auto_tune = toggle.car_make in {"hyundai", "toyota"} and FPCP.lateralTuning.which() == "torque"
|
||||
has_bsm = CP.enableBsm
|
||||
toggle.has_cc_long = toggle.car_make == "gm" and bool(CP.flags & GMFlags.CC_LONG.value)
|
||||
has_nnff = not comma_nnff_supported(toggle.car_model) and nnff_supported(toggle.car_model)
|
||||
has_nnff = nnff_supported(toggle.car_model)
|
||||
toggle.has_pedal = CP.enableGasInterceptor
|
||||
has_radar = not CP.radarUnavailable
|
||||
toggle.has_sdsu = toggle.car_make == "toyota" and bool(CP.flags & ToyotaFlags.SMART_DSU.value)
|
||||
has_sng = CP.autoResumeSng
|
||||
toggle.has_zss = toggle.car_make == "toyota" and bool(FPCP.fpFlags & ToyotaFrogPilotFlags.ZSS.value)
|
||||
is_angle_car = CP.steerControlType == car.CarParams.SteerControlType.angle
|
||||
latAccelFactor = CP.lateralTuning.torque.latAccelFactor
|
||||
latAccelFactor = FPCP.lateralTuning.torque.latAccelFactor
|
||||
longitudinalActuatorDelay = CP.longitudinalActuatorDelay
|
||||
max_acceleration_enabled = bool(CP.alternativeExperience & ALTERNATIVE_EXPERIENCE.RAISE_LONGITUDINAL_LIMITS_TO_ISO_MAX)
|
||||
toggle.openpilot_longitudinal = CP.openpilotLongitudinalControl and not toggle.disable_openpilot_long
|
||||
pcm_cruise = CP.pcmCruise
|
||||
startAccel = CP.startAccel
|
||||
stopAccel = CP.stopAccel
|
||||
steerActuatorDelay = CP.steerActuatorDelay
|
||||
steerKp = CP.lateralTuning.torque.kp
|
||||
steerKp = FPCP.lateralTuning.torque.kp
|
||||
steerRatio = CP.steerRatio
|
||||
toggle.stoppingDecelRate = CP.stoppingDecelRate
|
||||
taco_hacks_allowed = CP.safetyConfigs[0].safetyModel == SafetyModel.hyundaiCanfd
|
||||
@@ -595,7 +581,8 @@ class FrogPilotVariables:
|
||||
advanced_custom_ui = params.get_bool("AdvancedCustomUI") if tuning_level >= level["AdvancedCustomUI"] else default.get_bool("AdvancedCustomUI")
|
||||
toggle.hide_alerts = advanced_custom_ui and (params.get_bool("HideAlerts") if tuning_level >= level["HideAlerts"] else default.get_bool("HideAlerts")) and not toggle.debug_mode
|
||||
toggle.hide_lead_marker = toggle.openpilot_longitudinal and (advanced_custom_ui and (params.get_bool("HideLeadMarker") if tuning_level >= level["HideLeadMarker"] else default.get_bool("HideLeadMarker")) and not toggle.debug_mode)
|
||||
toggle.hide_map_icon = advanced_custom_ui and (params.get_bool("HideMapIcon") if tuning_level >= level["HideMapIcon"] else default.get_bool("HideMapIcon")) and not toggle.debug_mode
|
||||
toggle.hide_map_icon = advanced_custom_ui and (params.get_bool("HideMapIcon") if tuning_level >= level["HideMapIcon"] else default.get_bool("HideMapIcon"))
|
||||
toggle.hide_map = toggle.hide_map_icon and (params.get_bool("HideMap") if tuning_level >= level["HideMap"] else default.get_bool("HideMap"))
|
||||
toggle.hide_max_speed = advanced_custom_ui and (params.get_bool("HideMaxSpeed") if tuning_level >= level["HideMaxSpeed"] else default.get_bool("HideMaxSpeed")) and not toggle.debug_mode
|
||||
toggle.hide_speed = advanced_custom_ui and (params.get_bool("HideSpeed") if tuning_level >= level["HideSpeed"] else default.get_bool("HideSpeed")) and not toggle.debug_mode
|
||||
toggle.hide_speed_limit = advanced_custom_ui and (params.get_bool("HideSpeedLimit") if tuning_level >= level["HideSpeedLimit"] else default.get_bool("HideSpeedLimit")) and not toggle.debug_mode
|
||||
@@ -605,14 +592,14 @@ class FrogPilotVariables:
|
||||
toggle.force_auto_tune = advanced_lateral_tuning and not has_auto_tune and is_torque_car and (params.get_bool("ForceAutoTune") if tuning_level >= level["ForceAutoTune"] else default.get_bool("ForceAutoTune"))
|
||||
toggle.force_auto_tune_off = advanced_lateral_tuning and has_auto_tune and is_torque_car and (params.get_bool("ForceAutoTuneOff") if tuning_level >= level["ForceAutoTuneOff"] else default.get_bool("ForceAutoTuneOff"))
|
||||
toggle.steerActuatorDelay = np.clip(params.get_float("SteerDelay"), 0.01, 1.0) if advanced_lateral_tuning and tuning_level >= level["SteerDelay"] else steerActuatorDelay
|
||||
toggle.use_custom_steerActuatorDelay = bool(toggle.steerActuatorDelay != steerActuatorDelay)
|
||||
toggle.use_custom_steerActuatorDelay = bool(round(toggle.steerActuatorDelay, 2) != round(steerActuatorDelay, 2))
|
||||
toggle.friction = np.clip(params.get_float("SteerFriction"), 0, 0.5) if advanced_lateral_tuning and tuning_level >= level["SteerFriction"] else friction
|
||||
toggle.use_custom_friction = toggle.friction != friction and is_torque_car and not toggle.force_auto_tune or toggle.force_auto_tune_off
|
||||
toggle.use_custom_friction = bool(round(toggle.friction, 2) != round(friction, 2)) and is_torque_car and not toggle.force_auto_tune or toggle.force_auto_tune_off
|
||||
toggle.steerKp = [[0], [np.clip(params.get_float("SteerKP"), steerKp * 0.5, steerKp * 1.5) if advanced_lateral_tuning and is_torque_car and tuning_level >= level["SteerKP"] else steerKp]]
|
||||
toggle.latAccelFactor = np.clip(params.get_float("SteerLatAccel"), latAccelFactor * 0.75, latAccelFactor * 1.25) if advanced_lateral_tuning and tuning_level >= level["SteerLatAccel"] else latAccelFactor
|
||||
toggle.use_custom_latAccelFactor = toggle.latAccelFactor != latAccelFactor and is_torque_car and not toggle.force_auto_tune or toggle.force_auto_tune_off
|
||||
toggle.use_custom_latAccelFactor = bool(round(toggle.latAccelFactor, 2) != round(latAccelFactor, 2)) and is_torque_car and not toggle.force_auto_tune or toggle.force_auto_tune_off
|
||||
toggle.steerRatio = np.clip(params.get_float("SteerRatio"), steerRatio * 0.5, steerRatio * 1.5) if advanced_lateral_tuning and tuning_level >= level["SteerRatio"] else steerRatio
|
||||
toggle.use_custom_steerRatio = toggle.steerRatio != steerRatio and not toggle.force_auto_tune or toggle.force_auto_tune_off
|
||||
toggle.use_custom_steerRatio = bool(round(toggle.steerRatio, 2) != round(steerRatio, 2)) and not toggle.force_auto_tune or toggle.force_auto_tune_off
|
||||
|
||||
advanced_longitudinal_tuning = params.get_bool("AdvancedLongitudinalTune") if tuning_level >= level["AdvancedLongitudinalTune"] else default.get_bool("AdvancedLongitudinalTune")
|
||||
toggle.longitudinalActuatorDelay = np.clip(params.get_float("LongitudinalActuatorDelay"), 0, 1) if advanced_longitudinal_tuning and tuning_level >= level["LongitudinalActuatorDelay"] else longitudinalActuatorDelay
|
||||
@@ -632,7 +619,7 @@ class FrogPilotVariables:
|
||||
toggle.warningImmediate_volume = max(params.get_int("WarningImmediateVolume"), 25) if toggle.alert_volume_controller and tuning_level >= level["WarningImmediateVolume"] else default.get_int("WarningImmediateVolume")
|
||||
|
||||
toggle.always_on_lateral = params.get_bool("AlwaysOnLateral") if tuning_level >= level["AlwaysOnLateral"] else default.get_bool("AlwaysOnLateral")
|
||||
toggle.always_on_lateral_set = toggle.always_on_lateral and always_on_lateral_set
|
||||
toggle.always_on_lateral_set &= toggle.always_on_lateral
|
||||
toggle.always_on_lateral_lkas = toggle.always_on_lateral_set and toggle.use_lkas_for_aol and (params.get_bool("AlwaysOnLateralLKAS") if tuning_level >= level["AlwaysOnLateralLKAS"] else default.get_bool("AlwaysOnLateralLKAS"))
|
||||
toggle.always_on_lateral_main = toggle.always_on_lateral_set and not toggle.use_lkas_for_aol and (params.get_bool("AlwaysOnLateralMain") if tuning_level >= level["AlwaysOnLateralMain"] else default.get_bool("AlwaysOnLateralMain"))
|
||||
toggle.always_on_lateral_pause_speed = params.get_int("PauseAOLOnBrake") if toggle.always_on_lateral_set and tuning_level >= level["PauseAOLOnBrake"] else default.get_int("PauseAOLOnBrake")
|
||||
@@ -656,16 +643,11 @@ class FrogPilotVariables:
|
||||
toggle.conditional_navigation_lead = toggle.conditional_navigation and (params.get_bool("CENavigationLead") if tuning_level >= level["CENavigationLead"] else default.get_bool("CENavigationLead"))
|
||||
toggle.conditional_navigation_turns = toggle.conditional_navigation and (params.get_bool("CENavigationTurns") if tuning_level >= level["CENavigationTurns"] else default.get_bool("CENavigationTurns"))
|
||||
toggle.conditional_model_stop_time = params.get_int("CEModelStopTime") if toggle.conditional_experimental_mode and tuning_level >= level["CEModelStopTime"] else default.get_int("CEModelStopTime")
|
||||
toggle.conditional_signal = params.get_int("CESignalSpeed") if toggle.conditional_experimental_mode and tuning_level >= level["CESignalSpeed"] else default.get_int("CESignalSpeed")
|
||||
toggle.conditional_signal = params.get_int("CESignalSpeed") * speed_conversion if toggle.conditional_experimental_mode and tuning_level >= level["CESignalSpeed"] else default.get_int("CESignalSpeed") * CV.MPH_TO_MS
|
||||
toggle.conditional_signal_lane_detection = toggle.conditional_signal != 0 and (params.get_bool("CESignalLaneDetection") if tuning_level >= level["CESignalLaneDetection"] else default.get_bool("CESignalLaneDetection"))
|
||||
toggle.cem_status = toggle.conditional_experimental_mode and (params.get_bool("ShowCEMStatus") if tuning_level >= level["ShowCEMStatus"] else default.get_bool("ShowCEMStatus")) or toggle.debug_mode
|
||||
|
||||
toggle.curve_speed_controller = toggle.openpilot_longitudinal and (params.get_bool("CurveSpeedControl") if tuning_level >= level["CurveSpeedControl"] else default.get_bool("CurveSpeedControl"))
|
||||
toggle.curve_sensitivity = params.get_int("CurveSensitivity") / 100 if toggle.curve_speed_controller and tuning_level >= level["CurveSensitivity"] else default.get_int("CurveSensitivity") / 100
|
||||
toggle.turn_aggressiveness = params.get_int("TurnAggressiveness") / 100 if toggle.curve_speed_controller and tuning_level >= level["TurnAggressiveness"] else default.get_int("TurnAggressiveness") / 100
|
||||
toggle.map_turn_speed_controller = toggle.curve_speed_controller and (params.get_bool("MapTurnControl") if tuning_level >= level["MapTurnControl"] else default.get_bool("MapTurnControl"))
|
||||
toggle.mtsc_curvature_check = toggle.map_turn_speed_controller and (params.get_bool("MTSCCurvatureCheck") if tuning_level >= level["MTSCCurvatureCheck"] else default.get_bool("MTSCCurvatureCheck"))
|
||||
toggle.vision_turn_speed_controller = toggle.curve_speed_controller and (params.get_bool("VisionTurnControl") if tuning_level >= level["VisionTurnControl"] else default.get_bool("VisionTurnControl"))
|
||||
toggle.curve_speed_controller = toggle.openpilot_longitudinal and (params.get_bool("CurveSpeedController") if tuning_level >= level["CurveSpeedController"] else default.get_bool("CurveSpeedController"))
|
||||
toggle.csc_status = toggle.curve_speed_controller and (params.get_bool("ShowCSCStatus") if tuning_level >= level["ShowCSCStatus"] else default.get_bool("ShowCSCStatus")) or toggle.debug_mode
|
||||
|
||||
toggle.custom_alerts = params.get_bool("CustomAlerts") if tuning_level >= level["CustomAlerts"] else default.get_bool("CustomAlerts")
|
||||
@@ -682,28 +664,28 @@ class FrogPilotVariables:
|
||||
toggle.aggressive_jerk_danger = np.clip(params.get_int("AggressiveJerkDanger") / 100, 0.25, 2) if aggressive_profile and tuning_level >= level["AggressiveJerkDanger"] else default.get_int("AggressiveJerkDanger") / 100
|
||||
toggle.aggressive_jerk_speed = np.clip(params.get_int("AggressiveJerkSpeed") / 100, 0.25, 2) if aggressive_profile and tuning_level >= level["AggressiveJerkSpeed"] else default.get_int("AggressiveJerkSpeed") / 100
|
||||
toggle.aggressive_jerk_speed_decrease = np.clip(params.get_int("AggressiveJerkSpeedDecrease") / 100, 0.25, 2) if aggressive_profile and tuning_level >= level["AggressiveJerkSpeedDecrease"] else default.get_int("AggressiveJerkSpeedDecrease") / 100
|
||||
toggle.aggressive_follow = np.clip(params.get_float("AggressiveFollow"), 1, 5) if aggressive_profile and tuning_level >= level["AggressiveFollow"] else default.get_float("AggressiveFollow")
|
||||
toggle.aggressive_follow = np.clip(params.get_float("AggressiveFollow"), 1, MAX_T_FOLLOW) if aggressive_profile and tuning_level >= level["AggressiveFollow"] else default.get_float("AggressiveFollow")
|
||||
standard_profile = toggle.custom_personalities and (params.get_bool("StandardPersonalityProfile") if tuning_level >= level["StandardPersonalityProfile"] else default.get_bool("StandardPersonalityProfile"))
|
||||
toggle.standard_jerk_acceleration = np.clip(params.get_int("StandardJerkAcceleration") / 100, 0.25, 2) if standard_profile and tuning_level >= level["StandardJerkAcceleration"] else default.get_int("StandardJerkAcceleration") / 100
|
||||
toggle.standard_jerk_deceleration = np.clip(params.get_int("StandardJerkDeceleration") / 100, 0.25, 2) if standard_profile and tuning_level >= level["StandardJerkDeceleration"] else default.get_int("StandardJerkDeceleration") / 100
|
||||
toggle.standard_jerk_danger = np.clip(params.get_int("StandardJerkDanger") / 100, 0.25, 2) if standard_profile and tuning_level >= level["StandardJerkDanger"] else default.get_int("StandardJerkDanger") / 100
|
||||
toggle.standard_jerk_speed = np.clip(params.get_int("StandardJerkSpeed") / 100, 0.25, 2) if standard_profile and tuning_level >= level["StandardJerkSpeed"] else default.get_int("StandardJerkSpeed") / 100
|
||||
toggle.standard_jerk_speed_decrease = np.clip(params.get_int("StandardJerkSpeedDecrease") / 100, 0.25, 2) if standard_profile and tuning_level >= level["StandardJerkSpeedDecrease"] else default.get_int("StandardJerkSpeedDecrease") / 100
|
||||
toggle.standard_follow = np.clip(params.get_float("StandardFollow"), 1, 5) if standard_profile and tuning_level >= level["StandardFollow"] else default.get_float("StandardFollow")
|
||||
toggle.standard_follow = np.clip(params.get_float("StandardFollow"), 1, MAX_T_FOLLOW) if standard_profile and tuning_level >= level["StandardFollow"] else default.get_float("StandardFollow")
|
||||
relaxed_profile = toggle.custom_personalities and (params.get_bool("RelaxedPersonalityProfile") if tuning_level >= level["RelaxedPersonalityProfile"] else default.get_bool("RelaxedPersonalityProfile"))
|
||||
toggle.relaxed_jerk_acceleration = np.clip(params.get_int("RelaxedJerkAcceleration") / 100, 0.25, 2) if relaxed_profile and tuning_level >= level["RelaxedJerkAcceleration"] else default.get_int("RelaxedJerkAcceleration") / 100
|
||||
toggle.relaxed_jerk_deceleration = np.clip(params.get_int("RelaxedJerkDeceleration") / 100, 0.25, 2) if relaxed_profile and tuning_level >= level["RelaxedJerkDeceleration"] else default.get_int("RelaxedJerkDeceleration") / 100
|
||||
toggle.relaxed_jerk_danger = np.clip(params.get_int("RelaxedJerkDanger") / 100, 0.25, 2) if relaxed_profile and tuning_level >= level["RelaxedJerkDanger"] else default.get_int("RelaxedJerkDanger") / 100
|
||||
toggle.relaxed_jerk_speed = np.clip(params.get_int("RelaxedJerkSpeed") / 100, 0.25, 2) if relaxed_profile and tuning_level >= level["RelaxedJerkSpeed"] else default.get_int("RelaxedJerkSpeed") / 100
|
||||
toggle.relaxed_jerk_speed_decrease = np.clip(params.get_int("RelaxedJerkSpeedDecrease") / 100, 0.25, 2) if relaxed_profile and tuning_level >= level["RelaxedJerkSpeedDecrease"] else default.get_int("RelaxedJerkSpeedDecrease") / 100
|
||||
toggle.relaxed_follow = np.clip(params.get_float("RelaxedFollow"), 1, 5) if relaxed_profile and tuning_level >= level["RelaxedFollow"] else default.get_float("RelaxedFollow")
|
||||
toggle.relaxed_follow = np.clip(params.get_float("RelaxedFollow"), 1, MAX_T_FOLLOW) if relaxed_profile and tuning_level >= level["RelaxedFollow"] else default.get_float("RelaxedFollow")
|
||||
traffic_profile = toggle.custom_personalities and (params.get_bool("TrafficPersonalityProfile") if tuning_level >= level["TrafficPersonalityProfile"] else default.get_bool("TrafficPersonalityProfile"))
|
||||
toggle.traffic_mode_jerk_acceleration = [np.clip(params.get_int("TrafficJerkAcceleration") / 100, 0.25, 2) if traffic_profile and tuning_level >= level["TrafficJerkAcceleration"] else default.get_int("TrafficJerkAcceleration") / 100, toggle.aggressive_jerk_acceleration]
|
||||
toggle.traffic_mode_jerk_deceleration = [np.clip(params.get_int("TrafficJerkDeceleration") / 100, 0.25, 2) if traffic_profile and tuning_level >= level["TrafficJerkDeceleration"] else default.get_int("TrafficJerkDeceleration") / 100, toggle.aggressive_jerk_deceleration]
|
||||
toggle.traffic_mode_jerk_danger = [np.clip(params.get_int("TrafficJerkDanger") / 100, 0.25, 2) if traffic_profile and tuning_level >= level["TrafficJerkDanger"] else default.get_int("TrafficJerkDanger") / 100, toggle.aggressive_jerk_danger]
|
||||
toggle.traffic_mode_jerk_speed = [np.clip(params.get_int("TrafficJerkSpeed") / 100, 0.25, 2) if traffic_profile and tuning_level >= level["TrafficJerkSpeed"] else default.get_int("TrafficJerkSpeed") / 100, toggle.aggressive_jerk_speed]
|
||||
toggle.traffic_mode_jerk_speed_decrease = [np.clip(params.get_int("TrafficJerkSpeedDecrease") / 100, 0.25, 2) if traffic_profile and tuning_level >= level["TrafficJerkSpeedDecrease"] else default.get_int("TrafficJerkSpeedDecrease") / 100, toggle.aggressive_jerk_speed_decrease]
|
||||
toggle.traffic_mode_follow = [np.clip(params.get_float("TrafficFollow"), 0.5, 5) if traffic_profile and tuning_level >= level["TrafficFollow"] else default.get_float("TrafficFollow"), toggle.aggressive_follow]
|
||||
toggle.traffic_mode_follow = [np.clip(params.get_float("TrafficFollow"), 0.5, MAX_T_FOLLOW) if traffic_profile and tuning_level >= level["TrafficFollow"] else default.get_float("TrafficFollow"), toggle.aggressive_follow]
|
||||
|
||||
custom_ui = params.get_bool("CustomUI") if tuning_level >= level["CustomUI"] else default.get_bool("CustomUI")
|
||||
toggle.acceleration_path = toggle.openpilot_longitudinal and (custom_ui and (params.get_bool("AccelerationPath") if tuning_level >= level["AccelerationPath"] else default.get_bool("AccelerationPath")) or toggle.debug_mode)
|
||||
@@ -734,13 +716,13 @@ class FrogPilotVariables:
|
||||
toggle.storage_used_metrics = developer_metrics and (params.get_bool("ShowStorageUsed") if tuning_level >= level["ShowStorageUsed"] else default.get_bool("ShowStorageUsed")) and not toggle.debug_mode
|
||||
toggle.use_si_metrics = developer_metrics and (params.get_bool("UseSI") if tuning_level >= level["UseSI"] else default.get_bool("UseSI")) or toggle.debug_mode
|
||||
toggle.developer_sidebar = toggle.developer_ui and (params.get_bool("DeveloperSidebar") if tuning_level >= level["DeveloperSidebar"] else default.get_bool("DeveloperSidebar")) or toggle.debug_mode
|
||||
toggle.developer_sidebar_metric1 = params.get_int("DeveloperSidebarMetric1") if toggle.developer_sidebar and tuning_level >= level["DeveloperSidebarMetric1"] else 1 if toggle.debug_mode else default.get_float("DeveloperSidebarMetric1")
|
||||
toggle.developer_sidebar_metric2 = params.get_int("DeveloperSidebarMetric2") if toggle.developer_sidebar and tuning_level >= level["DeveloperSidebarMetric2"] else 3 if toggle.debug_mode else default.get_float("DeveloperSidebarMetric2")
|
||||
toggle.developer_sidebar_metric3 = params.get_int("DeveloperSidebarMetric3") if toggle.developer_sidebar and tuning_level >= level["DeveloperSidebarMetric3"] else 4 if toggle.debug_mode else default.get_float("DeveloperSidebarMetric3")
|
||||
toggle.developer_sidebar_metric4 = params.get_int("DeveloperSidebarMetric4") if toggle.developer_sidebar and tuning_level >= level["DeveloperSidebarMetric4"] else 5 if toggle.debug_mode else default.get_float("DeveloperSidebarMetric4")
|
||||
toggle.developer_sidebar_metric5 = params.get_int("DeveloperSidebarMetric5") if toggle.developer_sidebar and tuning_level >= level["DeveloperSidebarMetric5"] else 6 if toggle.debug_mode else default.get_float("DeveloperSidebarMetric5")
|
||||
toggle.developer_sidebar_metric6 = params.get_int("DeveloperSidebarMetric6") if toggle.developer_sidebar and tuning_level >= level["DeveloperSidebarMetric6"] else 7 if toggle.debug_mode else default.get_float("DeveloperSidebarMetric6")
|
||||
toggle.developer_sidebar_metric7 = params.get_int("DeveloperSidebarMetric7") if toggle.developer_sidebar and tuning_level >= level["DeveloperSidebarMetric7"] else 11 if toggle.debug_mode else default.get_float("DeveloperSidebarMetric7")
|
||||
toggle.developer_sidebar_metric1 = params.get_int("DeveloperSidebarMetric1") if toggle.developer_sidebar and tuning_level >= level["DeveloperSidebarMetric1"] else 1 if toggle.debug_mode else default.get_int("DeveloperSidebarMetric1")
|
||||
toggle.developer_sidebar_metric2 = params.get_int("DeveloperSidebarMetric2") if toggle.developer_sidebar and tuning_level >= level["DeveloperSidebarMetric2"] else 3 if toggle.debug_mode else default.get_int("DeveloperSidebarMetric2")
|
||||
toggle.developer_sidebar_metric3 = params.get_int("DeveloperSidebarMetric3") if toggle.developer_sidebar and tuning_level >= level["DeveloperSidebarMetric3"] else 4 if toggle.debug_mode else default.get_int("DeveloperSidebarMetric3")
|
||||
toggle.developer_sidebar_metric4 = params.get_int("DeveloperSidebarMetric4") if toggle.developer_sidebar and tuning_level >= level["DeveloperSidebarMetric4"] else 5 if toggle.debug_mode else default.get_int("DeveloperSidebarMetric4")
|
||||
toggle.developer_sidebar_metric5 = params.get_int("DeveloperSidebarMetric5") if toggle.developer_sidebar and tuning_level >= level["DeveloperSidebarMetric5"] else 6 if toggle.debug_mode else default.get_int("DeveloperSidebarMetric5")
|
||||
toggle.developer_sidebar_metric6 = params.get_int("DeveloperSidebarMetric6") if toggle.developer_sidebar and tuning_level >= level["DeveloperSidebarMetric6"] else 7 if toggle.debug_mode else default.get_int("DeveloperSidebarMetric6")
|
||||
toggle.developer_sidebar_metric7 = params.get_int("DeveloperSidebarMetric7") if toggle.developer_sidebar and tuning_level >= level["DeveloperSidebarMetric7"] else 11 if toggle.debug_mode else default.get_int("DeveloperSidebarMetric7")
|
||||
developer_widgets = toggle.developer_ui and params.get_bool("DeveloperWidgets") if tuning_level >= level["DeveloperWidgets"] else default.get_bool("DeveloperWidgets")
|
||||
toggle.adjacent_lead_tracking = has_radar and ((developer_widgets and params.get_bool("AdjacentLeadsUI") if tuning_level >= level["AdjacentLeadsUI"] else default.get_bool("AdjacentLeadsUI")) or toggle.debug_mode)
|
||||
toggle.radar_tracks = has_radar and ((developer_widgets and params.get_bool("RadarTracksUI") if tuning_level >= level["RadarTracksUI"] else default.get_bool("RadarTracksUI")) or toggle.debug_mode)
|
||||
@@ -798,7 +780,7 @@ class FrogPilotVariables:
|
||||
toggle.holiday_themes = params.get_bool("HolidayThemes") if tuning_level >= level["HolidayThemes"] else default.get_bool("HolidayThemes")
|
||||
toggle.current_holiday_theme = holiday_theme if toggle.holiday_themes else "stock"
|
||||
|
||||
toggle.lane_changes = params.get_bool("LaneChangeCustomizations") if tuning_level >= level["LaneChangeCustomizations"] else default.get_bool("LaneChangeCustomizations")
|
||||
toggle.lane_changes = params.get_bool("LaneChanges") if tuning_level >= level["LaneChanges"] else default.get_bool("LaneChanges")
|
||||
toggle.lane_change_delay = params.get_float("LaneChangeTime") if toggle.lane_changes and tuning_level >= level["LaneChangeTime"] else default.get_float("LaneChangeTime")
|
||||
toggle.lane_detection_width = params.get_float("LaneDetectionWidth") * distance_conversion if toggle.lane_changes and tuning_level >= level["LaneDetectionWidth"] else default.get_float("LaneDetectionWidth") * CV.FOOT_TO_METER
|
||||
toggle.lane_detection = toggle.lane_detection_width > 0
|
||||
@@ -807,6 +789,7 @@ class FrogPilotVariables:
|
||||
toggle.one_lane_change = toggle.lane_changes and (params.get_bool("OneLaneChange") if tuning_level >= level["OneLaneChange"] else default.get_bool("OneLaneChange"))
|
||||
|
||||
lateral_tuning = params.get_bool("LateralTune") if tuning_level >= level["LateralTune"] else default.get_bool("LateralTune")
|
||||
toggle.force_torque_controller = lateral_tuning and not is_torque_car and (params.get_bool("ForceTorqueController") if tuning_level >= level["ForceTorqueController"] else default.get_bool("ForceTorqueController"))
|
||||
toggle.nnff = lateral_tuning and has_nnff and not is_angle_car and (params.get_bool("NNFF") if tuning_level >= level["NNFF"] else default.get_bool("NNFF"))
|
||||
toggle.nnff_lite = not toggle.nnff and lateral_tuning and not is_angle_car and (params.get_bool("NNFFLite") if tuning_level >= level["NNFFLite"] else default.get_bool("NNFFLite"))
|
||||
toggle.use_turn_desires = lateral_tuning and (params.get_bool("TurnDesires") if tuning_level >= level["TurnDesires"] else default.get_bool("TurnDesires"))
|
||||
@@ -826,7 +809,6 @@ class FrogPilotVariables:
|
||||
|
||||
longitudinal_tuning = toggle.openpilot_longitudinal and (params.get_bool("LongitudinalTune") if tuning_level >= level["LongitudinalTune"] else default.get_bool("LongitudinalTune"))
|
||||
toggle.acceleration_profile = params.get_int("AccelerationProfile") if longitudinal_tuning and tuning_level >= level["AccelerationProfile"] else default.get_int("AccelerationProfile")
|
||||
toggle.sport_plus = max_acceleration_enabled and toggle.acceleration_profile == 3
|
||||
toggle.deceleration_profile = params.get_int("DecelerationProfile") if longitudinal_tuning and tuning_level >= level["DecelerationProfile"] else default.get_int("DecelerationProfile")
|
||||
toggle.human_acceleration = longitudinal_tuning and (params.get_bool("HumanAcceleration") if tuning_level >= level["HumanAcceleration"] else default.get_bool("HumanAcceleration"))
|
||||
toggle.human_following = longitudinal_tuning and (params.get_bool("HumanFollowing") if tuning_level >= level["HumanFollowing"] else default.get_bool("HumanFollowing"))
|
||||
@@ -834,39 +816,31 @@ class FrogPilotVariables:
|
||||
toggle.max_desired_acceleration = np.clip(params.get_float("MaxDesiredAcceleration"), 0.1, 4.0) if longitudinal_tuning and tuning_level >= level["MaxDesiredAcceleration"] else default.get_float("MaxDesiredAcceleration")
|
||||
toggle.taco_tune = longitudinal_tuning and (params.get_bool("TacoTune") if tuning_level >= level["TacoTune"] else default.get_bool("TacoTune"))
|
||||
|
||||
toggle.available_models = params.get("AvailableModels", encoding="utf-8") or ""
|
||||
toggle.available_model_names = params.get("AvailableModelNames", encoding="utf-8") or ""
|
||||
toggle.model_versions = params.get("ModelVersions", encoding="utf-8") or ""
|
||||
downloaded_models = [model for model in toggle.available_models.split(",") if any(MODELS_PATH.glob(f"{model}.*"))]
|
||||
toggle.model_randomizer = downloaded_models and (params.get_bool("ModelRandomizer") if tuning_level >= level["ModelRandomizer"] else default.get_bool("ModelRandomizer"))
|
||||
if toggle.available_models and toggle.available_model_names and downloaded_models and toggle.model_versions:
|
||||
toggle.available_models += f",{DEFAULT_TINYGRAD_MODEL}"
|
||||
toggle.available_model_names += f",{DEFAULT_TINYGRAD_MODEL_NAME}"
|
||||
toggle.model_versions += f",{DEFAULT_TINYGRAD_MODEL_VERSION}"
|
||||
downloaded_models += [DEFAULT_TINYGRAD_MODEL]
|
||||
if toggle.model_randomizer:
|
||||
if not started:
|
||||
blacklisted_models = (params.get("BlacklistedModels", encoding="utf-8") or "").split(",")
|
||||
selectable_models = [model for model in downloaded_models if model not in blacklisted_models]
|
||||
toggle.model = random.choice(selectable_models) if selectable_models else default.get("Model", encoding="utf-8")
|
||||
toggle.model_name = "Mystery Model 👻"
|
||||
toggle.model_version = toggle.model_versions.split(",")[toggle.available_models.split(",").index(toggle.model)]
|
||||
else:
|
||||
toggle.model = params.get("Model", encoding="utf-8") if tuning_level >= level["Model"] else default.get("Model", encoding="utf-8")
|
||||
if toggle.model in downloaded_models:
|
||||
toggle.model_name = toggle.available_model_names.split(",")[toggle.available_models.split(",").index(toggle.model)]
|
||||
toggle.model_version = toggle.model_versions.split(",")[toggle.available_models.split(",").index(toggle.model)]
|
||||
else:
|
||||
toggle.model = default.get("Model", encoding="utf-8")
|
||||
toggle.model_name = toggle.available_model_names.split(",")[toggle.available_models.split(",").index(toggle.model)]
|
||||
toggle.model_version = toggle.model_versions.split(",")[toggle.available_models.split(",").index(toggle.model)]
|
||||
toggle.available_models = (params.get("AvailableModels", encoding="utf-8") or "") + f",{DEFAULT_MODEL}"
|
||||
toggle.available_model_names = (params.get("AvailableModelNames", encoding="utf-8") or "") + f",{DEFAULT_MODEL_NAME}"
|
||||
downloaded_models = [model for model in toggle.available_models.split(",") if (MODELS_PATH / f"{model}.thneed").is_file() or all((MODELS_PATH / f"{model}_{filename}").is_file() for filename, _ in TINYGRAD_FILES)]
|
||||
model_versions = (params.get("ModelVersions", encoding="utf-8") or "") + f",{DEFAULT_MODEL_VERSION}"
|
||||
toggle.model_randomizer = params.get_bool("ModelRandomizer") if tuning_level >= level["ModelRandomizer"] else default.get_bool("ModelRandomizer")
|
||||
if toggle.model_randomizer:
|
||||
if not started:
|
||||
blacklisted_models = (params.get("BlacklistedModels", encoding="utf-8") or "").split(",")
|
||||
selectable_models = [model for model in downloaded_models if model not in blacklisted_models]
|
||||
toggle.model = random.choice(selectable_models) if selectable_models else DEFAULT_MODEL
|
||||
toggle.model_name = "Mystery Model 👻"
|
||||
toggle.model_version = model_versions.split(",")[toggle.available_models.split(",").index(toggle.model)]
|
||||
else:
|
||||
toggle.model = DEFAULT_CLASSIC_MODEL
|
||||
toggle.model_name = DEFAULT_CLASSIC_MODEL_NAME
|
||||
toggle.model_version = DEFAULT_CLASSIC_MODEL_VERSION
|
||||
model = ((params.get("Model", encoding="utf-8") if tuning_level >= level["Model"] else default.get("Model", encoding="utf-8")) or DEFAULT_MODEL).removesuffix("_default")
|
||||
if model in downloaded_models:
|
||||
toggle.model = model
|
||||
toggle.model_name = dict(zip(toggle.available_models.split(","), toggle.available_model_names.split(",")))[toggle.model]
|
||||
toggle.model_version = dict(zip(toggle.available_models.split(","), model_versions.split(",")))[toggle.model]
|
||||
else:
|
||||
toggle.model = DEFAULT_MODEL
|
||||
toggle.model_name = DEFAULT_MODEL_NAME
|
||||
toggle.model_version = DEFAULT_MODEL_VERSION
|
||||
toggle.classic_model = toggle.model_version in {"v1", "v2", "v3", "v4"}
|
||||
toggle.tinygrad_model = toggle.model_version in {"v7"}
|
||||
toggle.tomb_raider = toggle.model == "space-lab"
|
||||
toggle.classic_longitudinal = toggle.model_version in {"v1", "v2", "v3", "v4", "v5", "v6"}
|
||||
toggle.tinygrad_model = not toggle.classic_model and toggle.model_version not in {"v5", "v6"}
|
||||
|
||||
toggle.model_ui = params.get_bool("ModelUI") if tuning_level >= level["ModelUI"] else default.get_bool("ModelUI")
|
||||
toggle.dynamic_path_width = toggle.model_ui and (params.get_bool("DynamicPathWidth") if tuning_level >= level["DynamicPathWidth"] else default.get_bool("DynamicPathWidth"))
|
||||
@@ -884,8 +858,9 @@ class FrogPilotVariables:
|
||||
toggle.show_speed_limits = toggle.navigation_ui and (params.get_bool("ShowSpeedLimits") if tuning_level >= level["ShowSpeedLimits"] else default.get_bool("ShowSpeedLimits"))
|
||||
toggle.speed_limit_vienna = toggle.navigation_ui and (params.get_bool("UseVienna") if tuning_level >= level["UseVienna"] else default.get_bool("UseVienna"))
|
||||
|
||||
toggle.old_long_api = toggle.openpilot_longitudinal and toggle.car_make == "gm" and toggle.has_cc_long and not toggle.has_pedal
|
||||
toggle.old_long_api |= toggle.openpilot_longitudinal and toggle.car_make == "hyundai" and not (params.get_bool("NewLongAPI") if tuning_level >= level["NewLongAPI"] else default.get_bool("NewLongAPI"))
|
||||
if not started:
|
||||
toggle.old_long_api = toggle.openpilot_longitudinal and toggle.car_make == "gm" and toggle.has_cc_long and not toggle.has_pedal
|
||||
toggle.old_long_api |= toggle.openpilot_longitudinal and toggle.car_make == "hyundai" and not (params.get_bool("NewLongAPI") if tuning_level >= level["NewLongAPI"] else default.get_bool("NewLongAPI"))
|
||||
|
||||
personalize_openpilot = params.get_bool("PersonalizeOpenpilot") if tuning_level >= level["PersonalizeOpenpilot"] else default.get_bool("PersonalizeOpenpilot")
|
||||
toggle.color_scheme = toggle.current_holiday_theme if toggle.current_holiday_theme != "stock" else params.get("CustomColors", encoding="utf-8") if personalize_openpilot else "stock"
|
||||
@@ -894,10 +869,10 @@ class FrogPilotVariables:
|
||||
toggle.random_themes = personalize_openpilot and (params.get_bool("RandomThemes") if tuning_level >= level["RandomThemes"] else default.get_bool("RandomThemes"))
|
||||
toggle.signal_icons = toggle.current_holiday_theme if toggle.current_holiday_theme != "stock" else params.get("CustomSignals", encoding="utf-8") if personalize_openpilot else "stock"
|
||||
toggle.sound_pack = toggle.current_holiday_theme if toggle.current_holiday_theme != "stock" else params.get("CustomSounds", encoding="utf-8") if personalize_openpilot else "stock"
|
||||
if not toggle.random_themes or boot_run:
|
||||
if not toggle.random_themes:
|
||||
toggle.wheel_image = toggle.current_holiday_theme if toggle.current_holiday_theme != "stock" else params.get("WheelIcon", encoding="utf-8") if personalize_openpilot else "stock"
|
||||
else:
|
||||
toggle.wheel_image = next((file.resolve().stem for file in (ACTIVE_THEME_PATH / "steering_wheel").glob("wheel.*")), "none")
|
||||
toggle.wheel_image = next((file.resolve().stem for file in (ACTIVE_THEME_PATH / "steering_wheel").glob("wheel.*")), "stock")
|
||||
|
||||
quality_of_life_lateral = params.get_bool("QOLLateral") if tuning_level >= level["QOLLateral"] else default.get_bool("QOLLateral")
|
||||
toggle.pause_lateral_below_speed = params.get_int("PauseLateralSpeed") * speed_conversion if quality_of_life_lateral and tuning_level >= level["PauseLateralSpeed"] else default.get_int("PauseLateralSpeed") * CV.MPH_TO_MS
|
||||
@@ -906,9 +881,8 @@ class FrogPilotVariables:
|
||||
quality_of_life_longitudinal = params.get_bool("QOLLongitudinal") if tuning_level >= level["QOLLongitudinal"] else default.get_bool("QOLLongitudinal")
|
||||
toggle.cruise_increase = params.get_int("CustomCruise") if quality_of_life_longitudinal and not pcm_cruise and tuning_level >= level["CustomCruise"] else default.get_int("CustomCruise")
|
||||
toggle.cruise_increase_long = params.get_int("CustomCruiseLong") if quality_of_life_longitudinal and not pcm_cruise and tuning_level >= level["CustomCruiseLong"] else default.get_int("CustomCruiseLong")
|
||||
toggle.force_standstill = quality_of_life_longitudinal and (params.get_bool("ForceStandstill") if tuning_level >= level["ForceStandstill"] else default.get_bool("ForceStandstill"))
|
||||
toggle.force_stops = quality_of_life_longitudinal and (params.get_bool("ForceStops") if tuning_level >= level["ForceStops"] else default.get_bool("ForceStops"))
|
||||
toggle.increased_stopped_distance = params.get_int("IncreasedStoppedDistance") * distance_conversion if quality_of_life_longitudinal and tuning_level >= level["IncreasedStoppedDistance"] else default.get_int("IncreasedStoppedDistance") * CV.FOOT_TO_METER
|
||||
toggle.increase_stopped_distance = params.get_int("IncreasedStoppedDistance") * distance_conversion if quality_of_life_longitudinal and tuning_level >= level["IncreasedStoppedDistance"] else default.get_int("IncreasedStoppedDistance") * CV.FOOT_TO_METER
|
||||
map_gears = quality_of_life_longitudinal and (params.get_bool("MapGears") if tuning_level >= level["MapGears"] else default.get_bool("MapGears"))
|
||||
toggle.map_acceleration = map_gears and (params.get_bool("MapAcceleration") if tuning_level >= level["MapAcceleration"] else default.get_bool("MapAcceleration"))
|
||||
toggle.map_deceleration = map_gears and (params.get_bool("MapDeceleration") if tuning_level >= level["MapDeceleration"] else default.get_bool("MapDeceleration"))
|
||||
@@ -919,7 +893,6 @@ class FrogPilotVariables:
|
||||
toggle.camera_view = params.get_int("CameraView") if quality_of_life_visuals and tuning_level >= level["CameraView"] else default.get_int("CameraView")
|
||||
toggle.driver_camera_in_reverse = quality_of_life_visuals and (params.get_bool("DriverCamera") if tuning_level >= level["DriverCamera"] else default.get_bool("DriverCamera"))
|
||||
toggle.onroad_distance_button = toggle.openpilot_longitudinal and (quality_of_life_visuals and (params.get_bool("OnroadDistanceButton") if tuning_level >= level["OnroadDistanceButton"] else default.get_bool("OnroadDistanceButton")) or toggle.debug_mode)
|
||||
toggle.standby_mode = quality_of_life_visuals and (params.get_bool("StandbyMode") if tuning_level >= level["StandbyMode"] else default.get_bool("StandbyMode"))
|
||||
toggle.stopped_timer = quality_of_life_visuals and (params.get_bool("StoppedTimer") if tuning_level >= level["StoppedTimer"] else default.get_bool("StoppedTimer"))
|
||||
|
||||
toggle.rainbow_path = params.get_bool("RainbowPath") if tuning_level >= level["RainbowPath"] else default.get_bool("RainbowPath")
|
||||
@@ -932,6 +905,7 @@ class FrogPilotVariables:
|
||||
toggle.screen_recorder = screen_management and (params.get_bool("ScreenRecorder") if tuning_level >= level["ScreenRecorder"] else default.get_bool("ScreenRecorder")) or toggle.debug_mode
|
||||
toggle.screen_timeout = params.get_int("ScreenTimeout") if screen_management and tuning_level >= level["ScreenTimeout"] else default.get_int("ScreenTimeout")
|
||||
toggle.screen_timeout_onroad = params.get_int("ScreenTimeoutOnroad") if screen_management and tuning_level >= level["ScreenTimeoutOnroad"] else default.get_int("ScreenTimeoutOnroad")
|
||||
toggle.standby_mode = screen_management and (params.get_bool("StandbyMode") if tuning_level >= level["StandbyMode"] else default.get_bool("StandbyMode"))
|
||||
|
||||
toggle.sng_hack = toggle.openpilot_longitudinal and toggle.car_make == "toyota" and not toggle.has_pedal and not has_sng and (params.get_bool("SNGHack") if tuning_level >= level["SNGHack"] else default.get_bool("SNGHack"))
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
#!/usr/bin/env python3
|
||||
from cereal import car, custom
|
||||
from openpilot.selfdrive.controls.lib.drive_helpers import CRUISE_LONG_PRESS
|
||||
from openpilot.selfdrive.controls.lib.events import ET, EventName
|
||||
from openpilot.selfdrive.controls.lib.events import EventName
|
||||
|
||||
from openpilot.frogpilot.common.frogpilot_variables import NON_DRIVING_GEARS, params, params_memory
|
||||
|
||||
@@ -99,7 +99,7 @@ class FrogPilotCard:
|
||||
self.always_on_lateral_enabled &= sm["frogpilotPlan"].lateralCheck
|
||||
self.always_on_lateral_enabled &= sm["liveCalibration"].calPerc >= 1
|
||||
self.always_on_lateral_enabled &= not (carState.brakePressed and carState.vEgo < self.car.frogpilot_toggles.always_on_lateral_pause_speed) or carState.standstill
|
||||
self.always_on_lateral_enabled &= not any(getattr(event, ET.IMMEDIATE_DISABLE, False) for event in sm["onroadEvents"] if event.name != EventName.speedTooLow) or self.car.frogpilot_toggles.frogs_go_moo
|
||||
self.always_on_lateral_enabled &= not any(event.immediateDisable for events in (sm["onroadEvents"], sm["frogpilotOnroadEvents"]) for event in events if event.name != EventName.speedTooLow) or self.car.frogpilot_toggles.frogs_go_moo
|
||||
|
||||
if sm.updated["frogpilotPlan"] or any(be.type in (ButtonType.accelCruise, ButtonType.resumeCruise) for be in carState.buttonEvents):
|
||||
self.accel_pressed = any(be.type in (ButtonType.accelCruise, ButtonType.resumeCruise) for be in carState.buttonEvents)
|
||||
|
||||
@@ -8,7 +8,6 @@ from cereal import car, log
|
||||
from openpilot.common.conversions import Conversions as CV
|
||||
from openpilot.common.filter_simple import FirstOrderFilter
|
||||
from openpilot.common.realtime import DT_MDL
|
||||
|
||||
from openpilot.selfdrive.controls.lib.drive_helpers import V_CRUISE_MAX
|
||||
from openpilot.selfdrive.controls.lib.longitudinal_mpc_lib.long_mpc import A_CHANGE_COST, DANGER_ZONE_COST, J_EGO_COST, STOP_DISTANCE
|
||||
|
||||
@@ -21,17 +20,17 @@ from openpilot.frogpilot.controls.lib.frogpilot_following import FrogPilotFollow
|
||||
from openpilot.frogpilot.controls.lib.frogpilot_vcruise import FrogPilotVCruise
|
||||
|
||||
class FrogPilotPlanner:
|
||||
def __init__(self):
|
||||
def __init__(self, error_log, ThemeManager):
|
||||
self.cem = ConditionalExperimentalMode(self)
|
||||
self.frogpilot_acceleration = FrogPilotAcceleration(self)
|
||||
self.frogpilot_events = FrogPilotEvents(self)
|
||||
self.frogpilot_events = FrogPilotEvents(self, error_log, ThemeManager)
|
||||
self.frogpilot_following = FrogPilotFollowing(self)
|
||||
self.frogpilot_vcruise = FrogPilotVCruise(self)
|
||||
|
||||
with car.CarParams.from_bytes(params.get("CarParams", block=True)) as msg:
|
||||
self.CP = msg
|
||||
|
||||
self.tracking_lead_filter = FirstOrderFilter(0, 1, DT_MDL)
|
||||
self.tracking_lead_filter = FirstOrderFilter(0, 0.5, DT_MDL)
|
||||
|
||||
self.driving_in_curve = False
|
||||
self.lateral_check = False
|
||||
@@ -45,9 +44,10 @@ class FrogPilotPlanner:
|
||||
self.lateral_acceleration = 0
|
||||
self.model_length = 0
|
||||
self.road_curvature = 0
|
||||
self.time_to_curve = 0
|
||||
self.v_cruise = 0
|
||||
|
||||
def update(self, sm, frogpilot_toggles):
|
||||
def update(self, now, time_validated, sm, frogpilot_toggles):
|
||||
self.lead_one = sm["radarState"].leadOne
|
||||
|
||||
v_cruise = min(sm["controlsState"].vCruise, V_CRUISE_MAX) * CV.KPH_TO_MS
|
||||
@@ -104,23 +104,23 @@ class FrogPilotPlanner:
|
||||
self.model_stopped = self.model_length < CRUISING_SPEED * PLANNER_TIME
|
||||
self.model_stopped |= self.frogpilot_vcruise.forcing_stop
|
||||
|
||||
self.road_curvature = calculate_road_curvature(sm["modelV2"], v_ego)
|
||||
self.road_curvature, self.time_to_curve = calculate_road_curvature(sm["modelV2"], v_ego)
|
||||
|
||||
self.road_curvature_detected = (1 / abs(self.road_curvature))**0.5 < v_ego > CRUISING_SPEED and not (sm["carState"].leftBlinker or sm["carState"].rightBlinker)
|
||||
|
||||
if not sm["carState"].standstill:
|
||||
self.tracking_lead = self.update_lead_status()
|
||||
|
||||
self.v_cruise = self.frogpilot_vcruise.update(gps_position, v_cruise, v_ego, sm, frogpilot_toggles)
|
||||
self.v_cruise = self.frogpilot_vcruise.update(gps_position, now, time_validated, v_cruise, v_ego, sm, frogpilot_toggles)
|
||||
|
||||
def update_lead_status(self):
|
||||
following_lead = self.lead_one.status
|
||||
following_lead &= self.lead_one.dRel < self.model_length + STOP_DISTANCE
|
||||
|
||||
self.tracking_lead_filter.update(following_lead)
|
||||
return self.tracking_lead_filter.x >= THRESHOLD**2
|
||||
return self.tracking_lead_filter.x >= THRESHOLD
|
||||
|
||||
def publish(self, sm, pm, theme_updated, toggles_updated):
|
||||
def publish(self, theme_updated, toggles_updated, sm, pm, frogpilot_toggles):
|
||||
frogpilot_plan_send = messaging.new_message("frogpilotPlan")
|
||||
frogpilot_plan_send.valid = sm.all_checks(service_list=["carState", "controlsState"])
|
||||
frogpilotPlan = frogpilot_plan_send.frogpilotPlan
|
||||
@@ -132,6 +132,10 @@ class FrogPilotPlanner:
|
||||
frogpilotPlan.speedJerkStock = J_EGO_COST * self.frogpilot_following.base_speed_jerk
|
||||
frogpilotPlan.tFollow = self.frogpilot_following.t_follow
|
||||
|
||||
frogpilotPlan.cscControllingSpeed = self.frogpilot_vcruise.csc_controlling_speed
|
||||
frogpilotPlan.cscSpeed = self.frogpilot_vcruise.csc_target
|
||||
frogpilotPlan.cscTraining = self.frogpilot_vcruise.csc.enable_training
|
||||
|
||||
frogpilotPlan.desiredFollowDistance = self.frogpilot_following.desired_follow_distance
|
||||
|
||||
frogpilotPlan.experimentalMode = self.cem.experimental_mode or self.frogpilot_vcruise.slc.experimental_mode
|
||||
@@ -141,6 +145,8 @@ class FrogPilotPlanner:
|
||||
|
||||
frogpilotPlan.frogpilotEvents = self.frogpilot_events.events.to_msg()
|
||||
|
||||
frogpilotPlan.increasedStoppedDistance = frogpilot_toggles.increase_stopped_distance if not sm["frogpilotCarState"].trafficModeEnabled else 0
|
||||
|
||||
frogpilotPlan.laneWidthLeft = self.lane_width_left
|
||||
frogpilotPlan.laneWidthRight = self.lane_width_right
|
||||
|
||||
@@ -149,10 +155,6 @@ class FrogPilotPlanner:
|
||||
frogpilotPlan.maxAcceleration = self.frogpilot_acceleration.max_accel
|
||||
frogpilotPlan.minAcceleration = self.frogpilot_acceleration.min_accel
|
||||
|
||||
frogpilotPlan.mtscSpeed = self.frogpilot_vcruise.mtsc_target
|
||||
frogpilotPlan.vtscControllingCurve = self.frogpilot_vcruise.mtsc_target > self.frogpilot_vcruise.vtsc_target
|
||||
frogpilotPlan.vtscSpeed = self.frogpilot_vcruise.vtsc_target
|
||||
|
||||
frogpilotPlan.redLight = self.cem.stop_light_detected
|
||||
|
||||
frogpilotPlan.roadCurvature = self.road_curvature
|
||||
@@ -167,7 +169,7 @@ class FrogPilotPlanner:
|
||||
frogpilotPlan.speedLimitChanged = self.frogpilot_vcruise.slc.speed_limit_changed_timer > DT_MDL
|
||||
frogpilotPlan.unconfirmedSlcSpeedLimit = self.frogpilot_vcruise.slc.unconfirmed_speed_limit
|
||||
|
||||
frogpilotPlan.themeUpdated = theme_updated
|
||||
frogpilotPlan.themeUpdated = theme_updated or params_memory.get_bool("UseActiveTheme")
|
||||
|
||||
frogpilotPlan.togglesUpdated = toggles_updated
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ class ConditionalExperimentalMode:
|
||||
|
||||
self.curvature_filter = FirstOrderFilter(0, 1, DT_MDL)
|
||||
self.slow_lead_filter = FirstOrderFilter(0, 1, DT_MDL)
|
||||
self.stop_light_filter = FirstOrderFilter(0, 1, DT_MDL)
|
||||
self.stop_light_filter = FirstOrderFilter(0, 0.5, DT_MDL)
|
||||
|
||||
self.curve_detected = False
|
||||
self.experimental_mode = False
|
||||
@@ -24,7 +24,9 @@ class ConditionalExperimentalMode:
|
||||
|
||||
if self.status_value not in {1, 2} and not sm["carState"].standstill:
|
||||
self.update_conditions(v_ego, sm, frogpilot_toggles)
|
||||
|
||||
self.experimental_mode = self.check_conditions(v_ego, sm, frogpilot_toggles)
|
||||
|
||||
params_memory.put_int("CEStatus", self.status_value if self.experimental_mode else 0)
|
||||
else:
|
||||
self.experimental_mode = self.status_value == 2 or sm["carState"].standstill and self.experimental_mode and self.frogpilot_planner.model_stopped
|
||||
@@ -92,7 +94,7 @@ class ConditionalExperimentalMode:
|
||||
model_stopping = self.frogpilot_planner.model_length < v_ego * model_time
|
||||
|
||||
self.stop_light_filter.update(self.frogpilot_planner.model_stopped or model_stopping)
|
||||
self.stop_light_detected = self.stop_light_filter.x >= THRESHOLD**2 and not self.frogpilot_planner.tracking_lead
|
||||
self.stop_light_detected = self.stop_light_filter.x >= THRESHOLD and not self.frogpilot_planner.tracking_lead
|
||||
else:
|
||||
self.stop_light_filter.x = 0
|
||||
self.stop_light_detected = False
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
#!/usr/bin/env python3
|
||||
import json
|
||||
import numpy as np
|
||||
|
||||
from openpilot.common.realtime import DT_MDL
|
||||
|
||||
from openpilot.frogpilot.common.frogpilot_variables import CRUISING_SPEED, DEFAULT_LATERAL_ACCELERATION, PLANNER_TIME, params
|
||||
|
||||
CALIBRATION_PROGRESS_THRESHOLD = 10 / DT_MDL
|
||||
MAX_CURVATURE = 0.1
|
||||
MIN_CURVATURE = 0.001
|
||||
PERCENTILE = 90
|
||||
ROUNDING_PRECISION = 5
|
||||
STEP = 0.001
|
||||
|
||||
class CurveSpeedController:
|
||||
def __init__(self, FrogPilotVCruise):
|
||||
self.frogpilot_planner = FrogPilotVCruise.frogpilot_planner
|
||||
|
||||
self.enable_training = False
|
||||
self.target_set = False
|
||||
|
||||
self.training_timer = 0
|
||||
|
||||
self.curvature_data = json.loads(params.get("CurvatureData") or "{}")
|
||||
|
||||
self.required_curvatures = [str(round(road_curvature, ROUNDING_PRECISION)) for road_curvature in np.arange(MIN_CURVATURE, MAX_CURVATURE + STEP, STEP)]
|
||||
|
||||
self.update_lateral_acceleration()
|
||||
|
||||
def log_data(self, v_ego, sm):
|
||||
self.enable_training = v_ego > CRUISING_SPEED
|
||||
self.enable_training &= not self.frogpilot_planner.tracking_lead
|
||||
self.enable_training &= not sm["carControl"].longActive
|
||||
|
||||
if self.enable_training:
|
||||
self.training_timer += DT_MDL
|
||||
|
||||
if self.training_timer >= PLANNER_TIME and self.frogpilot_planner.driving_in_curve and not (sm["carState"].leftBlinker or sm["carState"].rightBlinker):
|
||||
lateral_acceleration = abs(self.frogpilot_planner.lateral_acceleration)
|
||||
road_curvature = abs(round(self.frogpilot_planner.road_curvature, ROUNDING_PRECISION))
|
||||
|
||||
key = str(road_curvature)
|
||||
if key in self.curvature_data:
|
||||
data = self.curvature_data[key]
|
||||
|
||||
average = data["average"]
|
||||
count = data["count"]
|
||||
|
||||
self.curvature_data[key] = {
|
||||
"average": ((average * count) + lateral_acceleration) / (count + 1),
|
||||
"count": count + 1
|
||||
}
|
||||
else:
|
||||
self.curvature_data[key] = {
|
||||
"average": lateral_acceleration,
|
||||
"count": 1
|
||||
}
|
||||
|
||||
self.update_lateral_acceleration()
|
||||
else:
|
||||
self.enable_training = False
|
||||
|
||||
elif self.training_timer >= PLANNER_TIME:
|
||||
progress = 0.0
|
||||
for key in self.required_curvatures:
|
||||
if key in self.curvature_data:
|
||||
progress += min(self.curvature_data[key]["count"] / CALIBRATION_PROGRESS_THRESHOLD, 1.0)
|
||||
|
||||
params.put_float_nonblocking("CalibrationProgress", (progress / len(self.required_curvatures)) * 100)
|
||||
params.put_nonblocking("CurvatureData", json.dumps(self.curvature_data))
|
||||
|
||||
self.enable_training = False
|
||||
|
||||
self.training_timer = 0
|
||||
|
||||
else:
|
||||
self.enable_training = False
|
||||
|
||||
self.training_timer = 0
|
||||
|
||||
def update_lateral_acceleration(self):
|
||||
if self.curvature_data:
|
||||
all_samples = [data["average"] for data in self.curvature_data.values()]
|
||||
self.lateral_acceleration = float(np.percentile(all_samples, PERCENTILE))
|
||||
else:
|
||||
self.lateral_acceleration = DEFAULT_LATERAL_ACCELERATION
|
||||
|
||||
params.put_float_nonblocking("CalibratedLateralAcceleration", self.lateral_acceleration)
|
||||
|
||||
def update_target(self, v_ego):
|
||||
lateral_acceleration = self.lateral_acceleration
|
||||
|
||||
if self.target_set:
|
||||
csc_speed = (lateral_acceleration / abs(self.frogpilot_planner.road_curvature))**0.5
|
||||
decel_rate = (v_ego - csc_speed) / self.frogpilot_planner.time_to_curve
|
||||
|
||||
self.target -= decel_rate * DT_MDL
|
||||
self.target = float(np.clip(self.target, CRUISING_SPEED, csc_speed))
|
||||
else:
|
||||
self.target_set = True
|
||||
|
||||
self.target = v_ego
|
||||
@@ -1,18 +1,18 @@
|
||||
#!/usr/bin/env python3
|
||||
import numpy as np
|
||||
|
||||
from openpilot.selfdrive.controls.lib.longitudinal_planner import A_CRUISE_MIN, get_max_accel
|
||||
from openpilot.selfdrive.controls.lib.longitudinal_mpc_lib.long_mpc import CRUISE_MIN_ACCEL
|
||||
from openpilot.selfdrive.controls.lib.longitudinal_planner import ACCEL_MIN, get_max_accel
|
||||
|
||||
from openpilot.frogpilot.common.frogpilot_variables import CITY_SPEED_LIMIT
|
||||
|
||||
A_CRUISE_MIN_ECO = A_CRUISE_MIN / 2
|
||||
A_CRUISE_MIN_SPORT = A_CRUISE_MIN * 2
|
||||
A_CRUISE_MIN_ECO = CRUISE_MIN_ACCEL / 2
|
||||
A_CRUISE_MIN_SPORT = CRUISE_MIN_ACCEL * 2
|
||||
|
||||
# MPH = [0.0, 11, 22, 34, 45, 56, 89]
|
||||
A_CRUISE_MAX_BP_CUSTOM = [0.0, 5., 10., 15., 20., 25., 40.]
|
||||
A_CRUISE_MAX_VALS_ECO = [2.0, 1.5, 1.0, 0.8, 0.6, 0.4, 0.2]
|
||||
A_CRUISE_MAX_VALS_SPORT = [3.0, 2.5, 2.0, 1.5, 1.0, 0.8, 0.6]
|
||||
A_CRUISE_MAX_VALS_SPORT_PLUS = [4.0, 3.5, 3.0, 2.5, 2.0, 1.5, 1.0]
|
||||
# MPH = [0.0, 11, 22, 34, 45, 56, 89]
|
||||
A_CRUISE_MAX_BP_CUSTOM = [0.0, 5., 10., 15., 20., 25., 40.]
|
||||
A_CRUISE_MAX_VALS_ECO = [2.0, 1.5, 1.0, 0.8, 0.6, 0.4, 0.2]
|
||||
A_CRUISE_MAX_VALS_SPORT = [3.0, 2.5, 2.0, 1.5, 1.0, 0.8, 0.6]
|
||||
|
||||
def get_max_accel_eco(v_ego):
|
||||
return float(np.interp(v_ego, A_CRUISE_MAX_BP_CUSTOM, A_CRUISE_MAX_VALS_ECO))
|
||||
@@ -20,14 +20,11 @@ def get_max_accel_eco(v_ego):
|
||||
def get_max_accel_sport(v_ego):
|
||||
return float(np.interp(v_ego, A_CRUISE_MAX_BP_CUSTOM, A_CRUISE_MAX_VALS_SPORT))
|
||||
|
||||
def get_max_accel_sport_plus(v_ego):
|
||||
return float(np.interp(v_ego, A_CRUISE_MAX_BP_CUSTOM, A_CRUISE_MAX_VALS_SPORT_PLUS))
|
||||
|
||||
def get_max_accel_low_speeds(max_accel, v_cruise):
|
||||
return float(np.interp(v_cruise, [0., CITY_SPEED_LIMIT / 2, CITY_SPEED_LIMIT], [max_accel / 4, max_accel / 2, max_accel]))
|
||||
|
||||
def get_max_accel_ramp_off(max_accel, v_cruise, v_ego):
|
||||
return float(np.interp(v_cruise - v_ego, [0., 1., 5., 10.], [0., 0.5, 1.0, max_accel]))
|
||||
return float(np.interp(v_cruise - v_ego, [0., 1., 5.], [0., 0.5, max_accel]))
|
||||
|
||||
def get_max_allowed_accel(v_ego):
|
||||
return float(np.interp(v_ego, [0., 5., 20.], [4.0, 4.0, 2.0])) # ISO 15622:2018
|
||||
@@ -49,17 +46,17 @@ class FrogPilotAcceleration:
|
||||
if eco_gear:
|
||||
self.max_accel = get_max_accel_eco(v_ego)
|
||||
else:
|
||||
if frogpilot_toggles.sport_plus:
|
||||
self.max_accel = get_max_accel_sport_plus(v_ego)
|
||||
else:
|
||||
if frogpilot_toggles.acceleration_profile == 2:
|
||||
self.max_accel = get_max_accel_sport(v_ego)
|
||||
else:
|
||||
self.max_accel = get_max_allowed_accel(v_ego)
|
||||
else:
|
||||
if frogpilot_toggles.acceleration_profile == 1:
|
||||
self.max_accel = get_max_accel_eco(v_ego)
|
||||
elif frogpilot_toggles.acceleration_profile == 2:
|
||||
self.max_accel = get_max_accel_sport(v_ego)
|
||||
elif frogpilot_toggles.sport_plus:
|
||||
self.max_accel = get_max_accel_sport_plus(v_ego)
|
||||
elif frogpilot_toggles.acceleration_profile == 3:
|
||||
self.max_accel = get_max_allowed_accel(v_ego)
|
||||
else:
|
||||
self.max_accel = get_max_accel(v_ego)
|
||||
|
||||
@@ -67,7 +64,9 @@ class FrogPilotAcceleration:
|
||||
self.max_accel = min(get_max_accel_low_speeds(self.max_accel, self.frogpilot_planner.v_cruise), self.max_accel)
|
||||
self.max_accel = min(get_max_accel_ramp_off(self.max_accel, self.frogpilot_planner.v_cruise, v_ego), self.max_accel)
|
||||
|
||||
if sm["frogpilotCarState"].forceCoast:
|
||||
if self.frogpilot_planner.tracking_lead:
|
||||
self.min_accel = ACCEL_MIN
|
||||
elif sm["frogpilotCarState"].forceCoast:
|
||||
self.min_accel = A_CRUISE_MIN_ECO
|
||||
elif frogpilot_toggles.map_deceleration and (eco_gear or sport_gear):
|
||||
if eco_gear:
|
||||
@@ -80,4 +79,4 @@ class FrogPilotAcceleration:
|
||||
elif frogpilot_toggles.deceleration_profile == 2:
|
||||
self.min_accel = A_CRUISE_MIN_SPORT
|
||||
else:
|
||||
self.min_accel = A_CRUISE_MIN
|
||||
self.min_accel = CRUISE_MIN_ACCEL
|
||||
|
||||
@@ -4,48 +4,40 @@ import random
|
||||
from openpilot.common.conversions import Conversions as CV
|
||||
from openpilot.common.realtime import DT_MDL
|
||||
from openpilot.selfdrive.controls.lib.desire_helper import TurnDirection
|
||||
from openpilot.selfdrive.controls.lib.events import ET, EVENTS, EventName, Events
|
||||
from openpilot.selfdrive.controls.lib.events import ET, EventName, FrogPilotEventName, Events
|
||||
from openpilot.selfdrive.controls.lib.vehicle_model import ACCELERATION_DUE_TO_GRAVITY
|
||||
|
||||
from openpilot.frogpilot.assets.theme_manager import update_wheel_image
|
||||
from openpilot.frogpilot.common.frogpilot_variables import CRUISING_SPEED, NON_DRIVING_GEARS, params, params_memory
|
||||
|
||||
DEJA_VU_G_FORCE = 0.5
|
||||
DEJA_VU_G_FORCE = 0.75
|
||||
RANDOM_EVENTS_CHANCE = 0.01 * DT_MDL
|
||||
|
||||
class FrogPilotEvents:
|
||||
def __init__(self, FrogPilotPlanner):
|
||||
def __init__(self, FrogPilotPlanner, error_log, ThemeManager):
|
||||
self.frogpilot_planner = FrogPilotPlanner
|
||||
self.theme_manager = ThemeManager
|
||||
|
||||
self.events = Events()
|
||||
self.error_log = error_log
|
||||
|
||||
self.events = Events(frogpilot=True)
|
||||
|
||||
self.accel30_played = False
|
||||
self.accel35_played = False
|
||||
self.accel40_played = False
|
||||
self.always_on_lateral_enabled_previously = False
|
||||
self.dejaVuCurve_played = False
|
||||
self.fcw_played = False
|
||||
self.firefoxSteerSaturated_played = False
|
||||
self.goatSteerSaturated_played = False
|
||||
self.hal9000_played = False
|
||||
self.holidayActive_played = False
|
||||
self.previous_traffic_mode = False
|
||||
self.random_event_playing = False
|
||||
self.startup_seen = False
|
||||
self.stopped_for_light = False
|
||||
self.thisIsFineSteerSaturated_played = False
|
||||
self.toBeContinued_played = False
|
||||
self.torqueNNLoad_played = False
|
||||
self.vCruise69_played = False
|
||||
self.yourFrogTriedToKillMe_played = False
|
||||
self.youveGotMail_played = False
|
||||
|
||||
self.max_acceleration = 0
|
||||
self.random_event_timer = 0
|
||||
self.tracking_lead_distance = 0
|
||||
self.tracked_lead_distance = 0
|
||||
|
||||
self.played_events = set()
|
||||
|
||||
def update(self, v_cruise, sm, frogpilot_toggles):
|
||||
alerts_empty = all(sm[state].alertText1 == "" and sm[state].alertText2 == "" for state in ["controlsState"])
|
||||
self.event_names = {event.name for event in sm["onroadEvents"]}
|
||||
self.frogpilot_event_names = {event.name for event in sm["frogpilotOnroadEvents"]}
|
||||
|
||||
alerts_empty = all(sm[state].alertText1 == "" and sm[state].alertText2 == "" for state in ["controlsState", "frogpilotControlsState"])
|
||||
|
||||
self.events.clear()
|
||||
|
||||
@@ -53,46 +45,50 @@ class FrogPilotEvents:
|
||||
self.random_event_timer += DT_MDL
|
||||
|
||||
if self.random_event_timer >= 5:
|
||||
update_wheel_image(frogpilot_toggles.wheel_image, frogpilot_toggles.current_holiday_theme, False)
|
||||
|
||||
self.theme_manager.update_wheel_image(frogpilot_toggles.wheel_image)
|
||||
params_memory.put_bool("UpdateWheelImage", True)
|
||||
|
||||
self.random_event_playing = False
|
||||
|
||||
self.random_event_timer = 0
|
||||
|
||||
if self.frogpilot_planner.frogpilot_vcruise.forcing_stop:
|
||||
self.events.add(EventName.forcingStop)
|
||||
if self.error_log.is_file():
|
||||
if frogpilot_toggles.random_events:
|
||||
self.events.add(FrogPilotEventName.openpilotCrashedRandomEvent)
|
||||
else:
|
||||
self.events.add(FrogPilotEventName.openpilotCrashed)
|
||||
|
||||
if not self.frogpilot_planner.tracking_lead and sm["carState"].standstill and sm["carState"].gearShifter not in NON_DRIVING_GEARS and frogpilot_toggles.green_light_alert:
|
||||
if not self.frogpilot_planner.model_stopped and self.stopped_for_light:
|
||||
self.events.add(EventName.greenLight)
|
||||
if self.frogpilot_planner.frogpilot_vcruise.forcing_stop:
|
||||
self.events.add(FrogPilotEventName.forcingStop)
|
||||
|
||||
if not self.frogpilot_planner.tracking_lead and sm["carState"].standstill and sm["carState"].gearShifter not in NON_DRIVING_GEARS:
|
||||
if not self.frogpilot_planner.model_stopped and self.stopped_for_light and frogpilot_toggles.green_light_alert:
|
||||
self.events.add(FrogPilotEventName.greenLight)
|
||||
|
||||
self.stopped_for_light = self.frogpilot_planner.cem.stop_light_detected
|
||||
else:
|
||||
self.stopped_for_light = False
|
||||
|
||||
if not self.holidayActive_played and self.startup_seen and alerts_empty and frogpilot_toggles.current_holiday_theme != "stock" and len(self.events) == 0:
|
||||
self.events.add(EventName.holidayActive)
|
||||
if "holidayActive" not in self.played_events and self.startup_seen and alerts_empty and frogpilot_toggles.current_holiday_theme != "stock" and len(self.events) == 0:
|
||||
self.events.add(FrogPilotEventName.holidayActive)
|
||||
|
||||
self.holidayActive_played = True
|
||||
self.played_events.add("holidayActive")
|
||||
|
||||
if self.frogpilot_planner.tracking_lead and sm["carState"].standstill and sm["carState"].gearShifter not in NON_DRIVING_GEARS and frogpilot_toggles.lead_departing_alert:
|
||||
if self.tracking_lead_distance == 0:
|
||||
self.tracking_lead_distance = self.frogpilot_planner.lead_one.dRel
|
||||
if self.tracked_lead_distance == 0:
|
||||
self.tracked_lead_distance = self.frogpilot_planner.lead_one.dRel
|
||||
|
||||
lead_departing = self.frogpilot_planner.lead_one.dRel - self.tracking_lead_distance > 1
|
||||
lead_departing = self.frogpilot_planner.lead_one.dRel - self.tracked_lead_distance > 1
|
||||
lead_departing &= self.frogpilot_planner.lead_one.vLead > 1
|
||||
|
||||
if lead_departing:
|
||||
self.events.add(EventName.leadDeparting)
|
||||
self.events.add(FrogPilotEventName.leadDeparting)
|
||||
else:
|
||||
self.tracking_lead_distance = 0
|
||||
self.tracked_lead_distance = 0
|
||||
|
||||
if not self.torqueNNLoad_played and self.startup_seen and alerts_empty and len(self.events) == 0 and params.get("NNFFModelName", encoding="utf-8") is not None and frogpilot_toggles.nnff:
|
||||
self.events.add(EventName.torqueNNLoad)
|
||||
if "torqueNNLoad" not in self.played_events and self.startup_seen and alerts_empty and len(self.events) == 0 and params.get("NNFFModelName", encoding="utf-8") is not None and frogpilot_toggles.nnff:
|
||||
self.events.add(FrogPilotEventName.torqueNNLoad)
|
||||
|
||||
self.torqueNNLoad_played = True
|
||||
self.played_events.add("torqueNNLoad")
|
||||
|
||||
if not self.random_event_playing and frogpilot_toggles.random_events:
|
||||
acceleration = sm["carState"].aEgo
|
||||
@@ -102,151 +98,125 @@ class FrogPilotEvents:
|
||||
else:
|
||||
self.max_acceleration = 0
|
||||
|
||||
if not self.accel30_played and 3.5 > self.max_acceleration >= 3.0 and acceleration < 1.5:
|
||||
self.events.add(EventName.accel30)
|
||||
|
||||
update_wheel_image("weeb_wheel")
|
||||
if "accel30" not in self.played_events and 3.5 > self.max_acceleration >= 3.0 and acceleration < 1.5:
|
||||
self.events.add(FrogPilotEventName.accel30)
|
||||
|
||||
self.theme_manager.update_wheel_image("weeb_wheel", random_event=True)
|
||||
params_memory.put_bool("UpdateWheelImage", True)
|
||||
|
||||
self.accel30_played = True
|
||||
self.random_event_playing = True
|
||||
|
||||
self.max_acceleration = 0
|
||||
self.played_events.add("accel30")
|
||||
|
||||
elif not self.accel35_played and 4.0 > self.max_acceleration >= 3.5 and acceleration < 1.5:
|
||||
self.events.add(EventName.accel35)
|
||||
|
||||
update_wheel_image("tree_fiddy")
|
||||
elif "accel35" not in self.played_events and 4.0 > self.max_acceleration >= 3.5 and acceleration < 1.5:
|
||||
self.events.add(FrogPilotEventName.accel35)
|
||||
|
||||
self.theme_manager.update_wheel_image("tree_fiddy", random_event=True)
|
||||
params_memory.put_bool("UpdateWheelImage", True)
|
||||
|
||||
self.accel35_played = True
|
||||
self.random_event_playing = True
|
||||
|
||||
self.max_acceleration = 0
|
||||
self.played_events.add("accel35")
|
||||
|
||||
elif not self.accel40_played and self.max_acceleration >= 4.0 and acceleration < 1.5:
|
||||
self.events.add(EventName.accel40)
|
||||
|
||||
update_wheel_image("great_scott")
|
||||
elif "accel40" not in self.played_events and self.max_acceleration >= 4.0 and acceleration < 1.5:
|
||||
self.events.add(FrogPilotEventName.accel40)
|
||||
|
||||
self.theme_manager.update_wheel_image("great_scott", random_event=True)
|
||||
params_memory.put_bool("UpdateWheelImage", True)
|
||||
|
||||
self.accel40_played = True
|
||||
self.random_event_playing = True
|
||||
|
||||
self.max_acceleration = 0
|
||||
self.played_events.add("accel40")
|
||||
|
||||
if not self.dejaVuCurve_played and sm["carState"].vEgo > CRUISING_SPEED:
|
||||
if "dejaVuCurve" not in self.played_events and sm["carState"].vEgo > CRUISING_SPEED:
|
||||
if self.frogpilot_planner.lateral_acceleration >= DEJA_VU_G_FORCE * ACCELERATION_DUE_TO_GRAVITY:
|
||||
self.events.add(EventName.dejaVuCurve)
|
||||
self.events.add(FrogPilotEventName.dejaVuCurve)
|
||||
|
||||
self.dejaVuCurve_played = True
|
||||
self.random_event_playing = True
|
||||
self.played_events.add("dejaVuCurve")
|
||||
|
||||
if not self.hal9000_played and sm["controlsState"].alertType == ET.NO_ENTRY:
|
||||
self.events.add(EventName.hal9000)
|
||||
if "hal9000" not in self.played_events and (sm["controlsState"].alertType == ET.NO_ENTRY or sm["frogpilotControlsState"].alertType == ET.NO_ENTRY):
|
||||
self.events.add(FrogPilotEventName.hal9000)
|
||||
|
||||
self.hal9000_played = True
|
||||
self.random_event_playing = True
|
||||
self.played_events.add("hal9000")
|
||||
|
||||
saturated_events = [
|
||||
("controlsState", [EventName.steerSaturated], EVENTS),
|
||||
("controlsState", [EventName.goatSteerSaturated], EVENTS)
|
||||
]
|
||||
saturated_alert_match = any(sm[state].alertText2 == events[event][ET.WARNING].alert_text_2 for state, event_group, events in saturated_events for event in event_group)
|
||||
if saturated_alert_match:
|
||||
if (EventName.steerSaturated in self.event_names or FrogPilotEventName.goatSteerSaturated in self.frogpilot_event_names):
|
||||
event_choices = []
|
||||
if not self.firefoxSteerSaturated_played:
|
||||
if "firefoxSteerSaturated" not in self.played_events:
|
||||
event_choices.append("firefoxSteerSaturated")
|
||||
if not self.goatSteerSaturated_played:
|
||||
if "goatSteerSaturated" not in self.played_events:
|
||||
event_choices.append("goatSteerSaturated")
|
||||
if not self.thisIsFineSteerSaturated_played:
|
||||
if "thisIsFineSteerSaturated" not in self.played_events:
|
||||
event_choices.append("thisIsFineSteerSaturated")
|
||||
|
||||
if event_choices and random.random() < RANDOM_EVENTS_CHANCE:
|
||||
event_choice = random.choice(event_choices)
|
||||
|
||||
if event_choice == "firefoxSteerSaturated":
|
||||
self.events.add(EventName.firefoxSteerSaturated)
|
||||
|
||||
update_wheel_image("firefox")
|
||||
self.events.add(FrogPilotEventName.firefoxSteerSaturated)
|
||||
|
||||
self.theme_manager.update_wheel_image("firefox", random_event=True)
|
||||
params_memory.put_bool("UpdateWheelImage", True)
|
||||
|
||||
self.firefoxSteerSaturated_played = True
|
||||
|
||||
elif event_choice == "goatSteerSaturated":
|
||||
self.events.add(EventName.goatSteerSaturated)
|
||||
|
||||
update_wheel_image("goat")
|
||||
self.events.add(FrogPilotEventName.goatSteerSaturated)
|
||||
|
||||
self.theme_manager.update_wheel_image("goat", random_event=True)
|
||||
params_memory.put_bool("UpdateWheelImage", True)
|
||||
|
||||
self.goatSteerSaturated_played = True
|
||||
|
||||
elif event_choice == "thisIsFineSteerSaturated":
|
||||
self.events.add(EventName.thisIsFineSteerSaturated)
|
||||
|
||||
update_wheel_image("this_is_fine")
|
||||
self.events.add(FrogPilotEventName.thisIsFineSteerSaturated)
|
||||
|
||||
self.theme_manager.update_wheel_image("this_is_fine", random_event=True)
|
||||
params_memory.put_bool("UpdateWheelImage", True)
|
||||
|
||||
self.thisIsFineSteerSaturated_played = True
|
||||
|
||||
self.random_event_playing = True
|
||||
self.played_events.add(event_choice)
|
||||
|
||||
if not self.vCruise69_played and 70 > max(sm["controlsState"].vCruise, sm["controlsState"].vCruiseCluster) * (1 if frogpilot_toggles.is_metric else CV.KPH_TO_MPH) >= 69:
|
||||
self.events.add(EventName.vCruise69)
|
||||
if "vCruise69" not in self.played_events and 70 > max(sm["controlsState"].vCruise, sm["controlsState"].vCruiseCluster) * (1 if frogpilot_toggles.is_metric else CV.KPH_TO_MPH) >= 69:
|
||||
self.events.add(FrogPilotEventName.vCruise69)
|
||||
|
||||
self.vCruise69_played = True
|
||||
self.random_event_playing = True
|
||||
self.played_events.add("vCruise69")
|
||||
|
||||
fcw_alert_match = sm["controlsState"].alertText1 == EVENTS[EventName.fcw][ET.PERMANENT].alert_text_1 and sm["controlsState"].alertText2 == EVENTS[EventName.fcw][ET.PERMANENT].alert_text_2
|
||||
stock_aeb_alert_match = sm["controlsState"].alertText1 == EVENTS[EventName.stockAeb][ET.PERMANENT].alert_text_1 and sm["controlsState"].alertText2 == EVENTS[EventName.stockAeb][ET.PERMANENT].alert_text_2
|
||||
if fcw_alert_match or stock_aeb_alert_match:
|
||||
event_choices = []
|
||||
if not self.toBeContinued_played:
|
||||
event_choices.append("toBeContinued")
|
||||
if not self.yourFrogTriedToKillMe_played:
|
||||
event_choices.append("yourFrogTriedToKillMe")
|
||||
if (EventName.fcw in self.event_names or EventName.stockAeb in self.event_names):
|
||||
event_choices = []
|
||||
if "toBeContinued" not in self.played_events:
|
||||
event_choices.append("toBeContinued")
|
||||
if "yourFrogTriedToKillMe" not in self.played_events:
|
||||
event_choices.append("yourFrogTriedToKillMe")
|
||||
|
||||
event_choice = random.choice(event_choices)
|
||||
if event_choice == "toBeContinued":
|
||||
self.events.add(EventName.toBeContinued)
|
||||
event_choice = random.choice(event_choices)
|
||||
if event_choice == "toBeContinued":
|
||||
self.events.add(FrogPilotEventName.toBeContinued)
|
||||
elif event_choice == "yourFrogTriedToKillMe":
|
||||
self.events.add(FrogPilotEventName.yourFrogTriedToKillMe)
|
||||
|
||||
self.toBeContinued_played = True
|
||||
self.random_event_playing = True
|
||||
self.played_events.add(event_choice)
|
||||
|
||||
elif event_choice == "yourFrogTriedToKillMe":
|
||||
self.events.add(EventName.yourFrogTriedToKillMe)
|
||||
|
||||
self.yourFrogTriedToKillMe_played = True
|
||||
|
||||
self.random_event_playing = True
|
||||
|
||||
if not self.youveGotMail_played and sm["frogpilotCarState"].alwaysOnLateralEnabled and not self.always_on_lateral_enabled_previously:
|
||||
if "youveGotMail" not in self.played_events and sm["frogpilotCarState"].alwaysOnLateralEnabled and not self.always_on_lateral_enabled_previously:
|
||||
if random.random() < RANDOM_EVENTS_CHANCE:
|
||||
self.events.add(EventName.youveGotMail)
|
||||
self.events.add(FrogPilotEventName.youveGotMail)
|
||||
|
||||
self.youveGotMail_played = True
|
||||
self.random_event_playing = True
|
||||
self.played_events.add("youveGotMail")
|
||||
|
||||
self.always_on_lateral_enabled_previously = sm["frogpilotCarState"].alwaysOnLateralEnabled
|
||||
|
||||
if frogpilot_toggles.speed_limit_changed_alert and self.frogpilot_planner.frogpilot_vcruise.slc.speed_limit_changed_timer == DT_MDL:
|
||||
self.events.add(EventName.speedLimitChanged)
|
||||
self.events.add(FrogPilotEventName.speedLimitChanged)
|
||||
|
||||
self.startup_seen |= sm["controlsState"].alertText1 == frogpilot_toggles.startup_alert_top and sm["controlsState"].alertText2 == frogpilot_toggles.startup_alert_bottom
|
||||
self.startup_seen |= sm["frogpilotControlsState"].alertText1 == frogpilot_toggles.startup_alert_top and sm["frogpilotControlsState"].alertText2 == frogpilot_toggles.startup_alert_bottom
|
||||
|
||||
if sm["frogpilotCarState"].trafficModeEnabled != self.previous_traffic_mode:
|
||||
if self.previous_traffic_mode:
|
||||
self.events.add(EventName.trafficModeInactive)
|
||||
self.events.add(FrogPilotEventName.trafficModeInactive)
|
||||
else:
|
||||
self.events.add(EventName.trafficModeActive)
|
||||
self.events.add(FrogPilotEventName.trafficModeActive)
|
||||
|
||||
self.previous_traffic_mode = sm["frogpilotCarState"].trafficModeEnabled
|
||||
|
||||
if sm["modelV2"].meta.turnDirection == TurnDirection.turnLeft:
|
||||
self.events.add(EventName.turningLeft)
|
||||
elif sm["modelV2"].meta.turnDirection == TurnDirection.turnRight:
|
||||
self.events.add(EventName.turningRight)
|
||||
if sm["frogpilotModelV2"].turnDirection == TurnDirection.turnLeft:
|
||||
self.events.add(FrogPilotEventName.turningLeft)
|
||||
elif sm["frogpilotModelV2"].turnDirection == TurnDirection.turnRight:
|
||||
self.events.add(FrogPilotEventName.turningRight)
|
||||
|
||||
@@ -1,71 +1,167 @@
|
||||
#!/usr/bin/env python3
|
||||
import json
|
||||
|
||||
from openpilot.common.conversions import Conversions as CV
|
||||
from openpilot.common.realtime import DT_MDL
|
||||
from openpilot.selfdrive.controls.controlsd import EventName, FrogPilotEventName, State
|
||||
from openpilot.selfdrive.controls.lib.drive_helpers import V_CRUISE_MAX
|
||||
from openpilot.selfdrive.ui.soundd import FrogPilotAudibleAlert
|
||||
|
||||
from openpilot.frogpilot.common.frogpilot_variables import params, params_tracking
|
||||
from openpilot.frogpilot.common.frogpilot_utilities import clean_model_name
|
||||
from openpilot.frogpilot.common.frogpilot_variables import params
|
||||
|
||||
RANDOM_EVENTS = {
|
||||
FrogPilotEventName.accel30: "accel30",
|
||||
FrogPilotEventName.accel35: "accel35",
|
||||
FrogPilotEventName.accel40: "accel40",
|
||||
FrogPilotEventName.dejaVuCurve: "dejaVuCurve",
|
||||
FrogPilotEventName.firefoxSteerSaturated: "firefoxSteerSaturated",
|
||||
FrogPilotEventName.hal9000: "hal9000",
|
||||
FrogPilotEventName.openpilotCrashedRandomEvent: "openpilotCrashedRandomEvent",
|
||||
FrogPilotEventName.thisIsFineSteerSaturated: "thisIsFineSteerSaturated",
|
||||
FrogPilotEventName.toBeContinued: "toBeContinued",
|
||||
FrogPilotEventName.vCruise69: "vCruise69",
|
||||
FrogPilotEventName.yourFrogTriedToKillMe: "yourFrogTriedToKillMe",
|
||||
FrogPilotEventName.youveGotMail: "youveGotMail",
|
||||
}
|
||||
|
||||
class FrogPilotTracking:
|
||||
def __init__(self):
|
||||
self.frogpilot_stats = json.loads(params.get("FrogPilotStats") or "{}")
|
||||
def __init__(self, frogpilot_planner, frogpilot_toggles):
|
||||
self.frogpilot_events = frogpilot_planner.frogpilot_events
|
||||
|
||||
self.total_drives = params_tracking.get_int("FrogPilotDrives")
|
||||
self.total_kilometers = params_tracking.get_float("FrogPilotKilometers")
|
||||
self.total_minutes = params_tracking.get_float("FrogPilotMinutes")
|
||||
self.frogpilot_stats = json.loads(params.get("FrogPilotStats") or "{}")
|
||||
self.frogpilot_stats.setdefault("AOLTime", self.frogpilot_stats.get("TotalAOLTime", 0))
|
||||
self.frogpilot_stats.setdefault("LateralTime", self.frogpilot_stats.get("TotalLateralTime", 0))
|
||||
self.frogpilot_stats.setdefault("LongitudinalTime", self.frogpilot_stats.get("TotalLongitudinalTime", 0))
|
||||
self.frogpilot_stats.setdefault("TrackedTime", self.frogpilot_stats.get("TotalTrackedTime", 0))
|
||||
|
||||
self.frogpilot_stats = {key: value for key, value in self.frogpilot_stats.items() if not key.startswith("Total")}
|
||||
|
||||
params.put("FrogPilotStats", json.dumps(self.frogpilot_stats))
|
||||
|
||||
self.drive_added = False
|
||||
self.enabled = False
|
||||
|
||||
self.aol_engaged_time = 0
|
||||
self.drive_distance = 0
|
||||
self.drive_time = 0
|
||||
self.lateral_engaged_time = 0
|
||||
self.longitudinal_engaged_time = 0
|
||||
self.distance_since_override = 0
|
||||
self.tracked_time = 0
|
||||
|
||||
self.total_aol_engaged = self.frogpilot_stats.get("TotalAOLTime", 0)
|
||||
self.total_lateral_engaged = self.frogpilot_stats.get("TotalLateralTime", 0)
|
||||
self.total_longitudinal_engaged = self.frogpilot_stats.get("TotalLongitudinalTime", 0)
|
||||
self.total_tracked_time = self.frogpilot_stats.get("TotalTrackedTime", 0)
|
||||
self.previous_events = set()
|
||||
self.previous_random_events = set()
|
||||
|
||||
self.sound = FrogPilotAudibleAlert.none
|
||||
self.state = State.disabled
|
||||
|
||||
self.model_name = clean_model_name(dict(zip(frogpilot_toggles.available_models.split(","), frogpilot_toggles.available_model_names.split(",")))[frogpilot_toggles.model])
|
||||
|
||||
def update(self, now, time_validated, sm, frogpilot_toggles):
|
||||
v_cruise = min(sm["controlsState"].vCruiseCluster, V_CRUISE_MAX) * CV.KPH_TO_MS
|
||||
v_ego = max(sm["carState"].vEgo, 0)
|
||||
|
||||
def update(self, sm):
|
||||
self.enabled |= sm["controlsState"].enabled or sm["frogpilotCarState"].alwaysOnLateralEnabled
|
||||
|
||||
self.drive_distance += sm["carState"].vEgo * DT_MDL
|
||||
self.drive_time += DT_MDL
|
||||
self.frogpilot_stats["FrogPilotMeters"] = self.frogpilot_stats.get("FrogPilotMeters", 0) + (v_ego * DT_MDL)
|
||||
|
||||
self.tracked_time += DT_MDL
|
||||
|
||||
self.frogpilot_stats["CurrentMonthsKilometers"] = self.frogpilot_stats.get("CurrentMonthsKilometers", 0) + (v_ego * DT_MDL) / 1000
|
||||
|
||||
self.frogpilot_stats["HighestAcceleration"] = max(self.frogpilot_events.max_acceleration, self.frogpilot_stats.get("HighestAcceleration", 0))
|
||||
|
||||
if sm["frogpilotControlsState"].alertSound != self.sound:
|
||||
if sm["frogpilotControlsState"].alertSound == FrogPilotAudibleAlert.goat:
|
||||
self.frogpilot_stats["GoatScreams"] = self.frogpilot_stats.get("GoatScreams", 0) + 1
|
||||
|
||||
self.sound = sm["frogpilotControlsState"].alertSound
|
||||
|
||||
if sm["controlsState"].enabled:
|
||||
key = str(round(v_cruise, 2))
|
||||
total_cruise_speed_times = self.frogpilot_stats.get("CruiseSpeedTimes", {})
|
||||
total_cruise_speed_times[key] = total_cruise_speed_times.get(key, 0) + DT_MDL
|
||||
self.frogpilot_stats["CruiseSpeedTimes"] = total_cruise_speed_times
|
||||
|
||||
if sm["carControl"].latActive:
|
||||
self.lateral_engaged_time += DT_MDL
|
||||
self.frogpilot_stats["LateralTime"] = self.frogpilot_stats.get("LateralTime", 0) + DT_MDL
|
||||
if sm["carControl"].longActive:
|
||||
self.longitudinal_engaged_time += DT_MDL
|
||||
self.frogpilot_stats["LongitudinalTime"] = self.frogpilot_stats.get("LongitudinalTime", 0) + DT_MDL
|
||||
elif sm["frogpilotCarState"].alwaysOnLateralEnabled:
|
||||
self.aol_engaged_time += DT_MDL
|
||||
self.frogpilot_stats["AOLTime"] = self.frogpilot_stats.get("AOLTime", 0) + DT_MDL
|
||||
|
||||
if self.drive_time > 60 and sm["carState"].standstill and self.enabled:
|
||||
self.total_kilometers += self.drive_distance / 1000
|
||||
params_tracking.put_float_nonblocking("FrogPilotKilometers", self.total_kilometers)
|
||||
self.drive_distance = 0
|
||||
if sm["carState"].standstill:
|
||||
self.frogpilot_stats["StandstillTime"] = self.frogpilot_stats.get("StandstillTime", 0) + DT_MDL
|
||||
if self.frogpilot_events.stopped_for_light:
|
||||
self.frogpilot_stats["StopLightTime"] = self.frogpilot_stats.get("StopLightTime", 0) + DT_MDL
|
||||
|
||||
self.total_minutes += self.drive_time / 60
|
||||
params_tracking.put_float_nonblocking("FrogPilotMinutes", self.total_minutes)
|
||||
if sm["controlsState"].experimentalMode:
|
||||
self.frogpilot_stats["ExperimentalModeTime"] = self.frogpilot_stats.get("ExperimentalModeTime", 0) + DT_MDL
|
||||
|
||||
self.total_aol_engaged += self.aol_engaged_time
|
||||
self.total_lateral_engaged += self.lateral_engaged_time
|
||||
self.total_longitudinal_engaged += self.longitudinal_engaged_time
|
||||
self.total_tracked_time += self.drive_time
|
||||
if sm["controlsState"].state in (State.disabled, State.overriding):
|
||||
self.distance_since_override = 0
|
||||
self.frogpilot_stats["OverrideTime"] = self.frogpilot_stats.get("OverrideTime", 0) + DT_MDL
|
||||
else:
|
||||
self.distance_since_override += v_ego * DT_MDL
|
||||
self.frogpilot_stats["LongestDistanceWithoutOverride"] = max(self.distance_since_override, self.frogpilot_stats.get("LongestDistanceWithoutOverride", 0))
|
||||
|
||||
self.frogpilot_stats["TotalAOLTime"] = self.total_aol_engaged
|
||||
self.frogpilot_stats["TotalLateralTime"] = self.total_lateral_engaged
|
||||
self.frogpilot_stats["TotalLongitudinalTime"] = self.total_longitudinal_engaged
|
||||
self.frogpilot_stats["TotalTrackedTime"] = self.total_tracked_time
|
||||
if sm["controlsState"].state != self.state:
|
||||
if sm["controlsState"].state == State.disabled:
|
||||
self.frogpilot_stats["Disengages"] = self.frogpilot_stats.get("Disengages", 0) + 1
|
||||
|
||||
params.put("FrogPilotStats", json.dumps(self.frogpilot_stats))
|
||||
if frogpilot_toggles.sound_pack == "frog":
|
||||
self.frogpilot_stats["FrogSqueaks"] = self.frogpilot_stats.get("FrogSqueaks", 0) + 1
|
||||
elif sm["controlsState"].state == State.enabled:
|
||||
self.frogpilot_stats["Engages"] = self.frogpilot_stats.get("Engages", 0) + 1
|
||||
|
||||
self.aol_engaged_time = 0
|
||||
self.drive_time = 0
|
||||
self.lateral_engaged_time = 0
|
||||
self.longitudinal_engaged_time = 0
|
||||
if frogpilot_toggles.sound_pack == "frog":
|
||||
self.frogpilot_stats["FrogChirps"] = self.frogpilot_stats.get("FrogChirps", 0) + 1
|
||||
elif sm["controlsState"].state == State.overriding:
|
||||
self.frogpilot_stats["Overrides"] = self.frogpilot_stats.get("Overrides", 0) + 1
|
||||
|
||||
self.state = sm["controlsState"].state
|
||||
|
||||
current_events = {event for event in self.frogpilot_events.event_names}
|
||||
if len(current_events) > 0:
|
||||
new_events = current_events - self.previous_events
|
||||
|
||||
if new_events:
|
||||
if (EventName.fcw in self.frogpilot_events.event_names or EventName.stockAeb in self.frogpilot_events.event_names):
|
||||
self.frogpilot_stats["AEBEvents"] = self.frogpilot_stats.get("AEBEvents", 0) + 1
|
||||
|
||||
self.previous_events = current_events
|
||||
|
||||
current_random_events = {event for event in self.frogpilot_events.events.names if event in RANDOM_EVENTS}
|
||||
if len(current_random_events) > 0:
|
||||
new_events = current_random_events - self.previous_random_events
|
||||
|
||||
if new_events:
|
||||
total_random_events = self.frogpilot_stats.get("RandomEvents", {})
|
||||
for event in new_events:
|
||||
event_name = RANDOM_EVENTS[event]
|
||||
total_random_events[event_name] = total_random_events.get(event_name, 0) + 1
|
||||
self.frogpilot_stats["RandomEvents"] = total_random_events
|
||||
|
||||
self.previous_random_events = current_random_events
|
||||
|
||||
if self.tracked_time > 60 and sm["carState"].standstill and self.enabled:
|
||||
if time_validated:
|
||||
current_month = now.month
|
||||
if current_month != self.frogpilot_stats.get("Month"):
|
||||
self.frogpilot_stats.update({
|
||||
"CurrentMonthsKilometers": 0,
|
||||
"Month": current_month
|
||||
})
|
||||
|
||||
self.frogpilot_stats["FrogPilotSeconds"] = self.frogpilot_stats.get("FrogPilotSeconds", 0) + self.tracked_time
|
||||
|
||||
self.frogpilot_stats["TrackedTime"] = self.frogpilot_stats.get("TrackedTime", 0) + self.tracked_time
|
||||
|
||||
current_model = self.model_name
|
||||
total_model_times = self.frogpilot_stats.get("ModelTimes", {})
|
||||
total_model_times[current_model] = total_model_times.get(current_model, 0) + self.tracked_time
|
||||
self.frogpilot_stats["ModelTimes"] = total_model_times
|
||||
|
||||
self.tracked_time = 0
|
||||
|
||||
if not self.drive_added:
|
||||
self.total_drives += 1
|
||||
params_tracking.put_int_nonblocking("FrogPilotDrives", self.total_drives)
|
||||
self.frogpilot_stats["FrogPilotDrives"] = self.frogpilot_stats.get("FrogPilotDrives", 0) + 1
|
||||
self.drive_added = True
|
||||
|
||||
params.put_nonblocking("FrogPilotStats", json.dumps(self.frogpilot_stats))
|
||||
|
||||
@@ -4,25 +4,22 @@ from openpilot.common.realtime import DT_MDL
|
||||
from openpilot.selfdrive.controls.lib.longitudinal_mpc_lib.long_mpc import COMFORT_BRAKE
|
||||
|
||||
from openpilot.frogpilot.common.frogpilot_variables import CRUISING_SPEED, PLANNER_TIME
|
||||
from openpilot.frogpilot.controls.lib.map_turn_speed_controller import MapTurnSpeedController
|
||||
from openpilot.frogpilot.controls.lib.curve_speed_controller import CurveSpeedController
|
||||
from openpilot.frogpilot.controls.lib.speed_limit_controller import SpeedLimitController
|
||||
|
||||
TARGET_LAT_A = 2.0
|
||||
|
||||
class FrogPilotVCruise:
|
||||
def __init__(self, FrogPilotPlanner):
|
||||
self.frogpilot_planner = FrogPilotPlanner
|
||||
|
||||
self.mtsc = MapTurnSpeedController()
|
||||
self.csc = CurveSpeedController(self)
|
||||
self.slc = SpeedLimitController()
|
||||
|
||||
self.forcing_stop = False
|
||||
self.override_force_stop = False
|
||||
|
||||
self.mtsc_target = 0
|
||||
self.override_force_stop_timer = 0
|
||||
|
||||
def update(self, gps_position, v_cruise, v_ego, sm, frogpilot_toggles):
|
||||
def update(self, gps_position, now, time_validated, v_cruise, v_ego, sm, frogpilot_toggles):
|
||||
force_stop = self.frogpilot_planner.cem.stop_light_detected and sm["controlsState"].enabled and frogpilot_toggles.force_stops
|
||||
force_stop &= self.frogpilot_planner.model_stopped
|
||||
force_stop &= self.override_force_stop_timer <= 0
|
||||
@@ -46,6 +43,21 @@ class FrogPilotVCruise:
|
||||
v_ego_cluster = max(sm["carState"].vEgoCluster, v_ego)
|
||||
v_ego_diff = v_ego_cluster - v_ego
|
||||
|
||||
# FrogsGoMoo's Curve Speed Controller
|
||||
if v_ego > CRUISING_SPEED and sm["controlsState"].enabled and self.frogpilot_planner.road_curvature_detected and frogpilot_toggles.curve_speed_controller:
|
||||
self.csc.update_target(v_ego)
|
||||
|
||||
self.csc_controlling_speed = True
|
||||
|
||||
self.csc_target = self.csc.target
|
||||
else:
|
||||
self.csc.log_data(v_ego, sm)
|
||||
|
||||
self.csc_controlling_speed = False
|
||||
self.csc.target_set = False
|
||||
|
||||
self.csc_target = v_cruise
|
||||
|
||||
# Mike's extended lead linear braking
|
||||
if self.frogpilot_planner.lead_one.vLead < v_ego > CRUISING_SPEED and sm["controlsState"].enabled and self.frogpilot_planner.tracking_lead and frogpilot_toggles.human_following:
|
||||
if not self.frogpilot_planner.frogpilot_following.following_lead:
|
||||
@@ -56,31 +68,17 @@ class FrogPilotVCruise:
|
||||
else:
|
||||
self.braking_target = v_cruise
|
||||
|
||||
# Pfeiferj's Map Turn Speed Controller
|
||||
if v_ego > CRUISING_SPEED and sm["controlsState"].enabled and frogpilot_toggles.map_turn_speed_controller:
|
||||
mtsc_active = self.mtsc_target < v_cruise
|
||||
|
||||
if self.frogpilot_planner.road_curvature_detected and mtsc_active:
|
||||
self.mtsc_target = self.mtsc_target
|
||||
elif not self.frogpilot_planner.road_curvature_detected and frogpilot_toggles.mtsc_curvature_check:
|
||||
self.mtsc_target = v_cruise
|
||||
else:
|
||||
mtsc_speed = ((TARGET_LAT_A * frogpilot_toggles.turn_aggressiveness) / (self.mtsc.get_map_curvature(gps_position, v_ego) * frogpilot_toggles.curve_sensitivity))**0.5
|
||||
self.mtsc_target = max(CRUISING_SPEED, mtsc_speed)
|
||||
else:
|
||||
self.mtsc_target = v_cruise
|
||||
|
||||
# Pfeiferj's Speed Limit Controller
|
||||
self.slc.frogpilot_toggles = frogpilot_toggles
|
||||
|
||||
if frogpilot_toggles.speed_limit_controller:
|
||||
self.slc.update_limits(sm["frogpilotCarState"].dashboardSpeedLimit, gps_position, sm["frogpilotNavigation"].navigationSpeedLimit, v_cruise, v_ego, sm)
|
||||
self.slc.update_limits(sm["frogpilotCarState"].dashboardSpeedLimit, gps_position, sm["frogpilotNavigation"].navigationSpeedLimit, now, time_validated, v_cruise, v_ego, sm)
|
||||
self.slc.update_override(v_cruise, v_cruise_diff, v_ego, v_ego_diff, sm)
|
||||
|
||||
self.slc_offset = self.slc.offset
|
||||
self.slc_target = self.slc.target
|
||||
elif frogpilot_toggles.show_speed_limits:
|
||||
self.slc.update_limits(sm["frogpilotCarState"].dashboardSpeedLimit, gps_position, sm["frogpilotNavigation"].navigationSpeedLimit, v_cruise, v_ego, sm)
|
||||
self.slc.update_limits(sm["frogpilotCarState"].dashboardSpeedLimit, gps_position, sm["frogpilotNavigation"].navigationSpeedLimit, now, time_validated, v_cruise, v_ego, sm)
|
||||
|
||||
self.slc_offset = 0
|
||||
self.slc_target = self.slc.target
|
||||
@@ -88,19 +86,7 @@ class FrogPilotVCruise:
|
||||
self.slc_offset = 0
|
||||
self.slc_target = 0
|
||||
|
||||
# Pfeiferj's Vision Turn Controller
|
||||
if v_ego > CRUISING_SPEED and sm["controlsState"].enabled and self.frogpilot_planner.road_curvature_detected and frogpilot_toggles.vision_turn_speed_controller:
|
||||
vtsc_speed = ((TARGET_LAT_A * frogpilot_toggles.turn_aggressiveness) / (abs(self.frogpilot_planner.road_curvature) * frogpilot_toggles.curve_sensitivity))**0.5
|
||||
self.vtsc_target = max(CRUISING_SPEED, vtsc_speed)
|
||||
else:
|
||||
self.vtsc_target = v_cruise
|
||||
|
||||
if sm["carState"].standstill and not self.override_force_stop and sm["controlsState"].enabled and frogpilot_toggles.force_standstill:
|
||||
self.forcing_stop = True
|
||||
|
||||
v_cruise = -1
|
||||
|
||||
elif force_stop_enabled and not self.override_force_stop:
|
||||
if force_stop_enabled and not self.override_force_stop:
|
||||
self.forcing_stop |= not sm["carState"].standstill
|
||||
|
||||
self.tracked_model_length = max(self.tracked_model_length - (v_ego * DT_MDL), 0)
|
||||
@@ -111,13 +97,10 @@ class FrogPilotVCruise:
|
||||
|
||||
self.tracked_model_length = self.frogpilot_planner.model_length
|
||||
|
||||
targets = [self.braking_target, self.mtsc_target, self.vtsc_target, v_cruise]
|
||||
targets = [self.braking_target, self.csc_target, v_cruise]
|
||||
if frogpilot_toggles.speed_limit_controller:
|
||||
targets.append(max(self.slc.overridden_speed, self.slc_target + self.slc_offset) - v_ego_diff)
|
||||
|
||||
v_cruise = min([target if target > CRUISING_SPEED else v_cruise for target in targets])
|
||||
|
||||
self.mtsc_target += v_cruise_diff
|
||||
self.vtsc_target += v_cruise_diff
|
||||
|
||||
return v_cruise
|
||||
|
||||
@@ -1,82 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# PFEIFER - MTSC - Modified by FrogAi for FrogPilot
|
||||
import json
|
||||
import math
|
||||
|
||||
from openpilot.common.conversions import Conversions as CV
|
||||
|
||||
from openpilot.frogpilot.common.frogpilot_utilities import calculate_distance_to_point
|
||||
from openpilot.frogpilot.common.frogpilot_variables import PLANNER_TIME, params_memory
|
||||
|
||||
def calculate_curvature(p1, p2, p3):
|
||||
lat1, lon1 = p1
|
||||
lat2, lon2 = p2
|
||||
lat3, lon3 = p3
|
||||
|
||||
lat1_rad, lon1_rad = lat1 * CV.DEG_TO_RAD, lon1 * CV.DEG_TO_RAD
|
||||
lat2_rad, lon2_rad = lat2 * CV.DEG_TO_RAD, lon2 * CV.DEG_TO_RAD
|
||||
lat3_rad, lon3_rad = lat3 * CV.DEG_TO_RAD, lon3 * CV.DEG_TO_RAD
|
||||
|
||||
side_a = calculate_distance_to_point(lat2_rad, lon2_rad, lat3_rad, lon3_rad)
|
||||
side_b = calculate_distance_to_point(lat1_rad, lon1_rad, lat3_rad, lon3_rad)
|
||||
side_c = calculate_distance_to_point(lat1_rad, lon1_rad, lat2_rad, lon2_rad)
|
||||
|
||||
s = (side_a + side_b + side_c) / 2
|
||||
|
||||
area_squared = s * (s - side_a) * (s - side_b) * (s - side_c)
|
||||
if area_squared <= 0:
|
||||
return 0
|
||||
|
||||
area = math.sqrt(area_squared)
|
||||
|
||||
radius = (side_a * side_b * side_c) / (4 * area)
|
||||
if radius == 0:
|
||||
return 0
|
||||
|
||||
curvature = 1 / radius
|
||||
return curvature
|
||||
|
||||
class MapTurnSpeedController:
|
||||
def get_map_curvature(self, gps_position, v_ego):
|
||||
if not gps_position:
|
||||
return 1e-6
|
||||
current_latitude = gps_position["latitude"]
|
||||
current_longitude = gps_position["longitude"]
|
||||
|
||||
distances = []
|
||||
minimum_idx = 0
|
||||
minimum_distance = 1000.0
|
||||
|
||||
target_velocities = json.loads(params_memory.get("MapTargetVelocities") or "[]")
|
||||
for i, target_velocity in enumerate(target_velocities):
|
||||
target_latitude = target_velocity["latitude"]
|
||||
target_longitude = target_velocity["longitude"]
|
||||
|
||||
distance = calculate_distance_to_point(current_latitude * CV.DEG_TO_RAD, current_longitude * CV.DEG_TO_RAD, target_latitude * CV.DEG_TO_RAD, target_longitude * CV.DEG_TO_RAD)
|
||||
distances.append(distance)
|
||||
|
||||
if distance < minimum_distance:
|
||||
minimum_distance = distance
|
||||
minimum_idx = i
|
||||
|
||||
forward_distances = distances[minimum_idx:]
|
||||
|
||||
cumulative_distance = 0.0
|
||||
target_idx = None
|
||||
|
||||
for i, distance in enumerate(forward_distances):
|
||||
cumulative_distance += distance
|
||||
if cumulative_distance >= PLANNER_TIME * v_ego:
|
||||
target_idx = i
|
||||
break
|
||||
|
||||
forward_points = target_velocities[minimum_idx:]
|
||||
|
||||
if target_idx is None or target_idx == 0 or target_idx >= len(forward_points) - 1:
|
||||
return 1e-6
|
||||
|
||||
p1 = (forward_points[target_idx - 1]["latitude"], forward_points[target_idx - 1]["longitude"])
|
||||
p2 = (forward_points[target_idx]["latitude"], forward_points[target_idx]["longitude"])
|
||||
p3 = (forward_points[target_idx + 1]["latitude"], forward_points[target_idx + 1]["longitude"])
|
||||
|
||||
return max(calculate_curvature(p1, p2, p3), 1e-6)
|
||||
@@ -2,6 +2,7 @@
|
||||
# Twilsonco's Lateral Neural Network Feedforward
|
||||
from collections import deque
|
||||
from difflib import SequenceMatcher
|
||||
from typing import NamedTuple
|
||||
|
||||
import json
|
||||
import math
|
||||
@@ -10,11 +11,10 @@ import os
|
||||
|
||||
from openpilot.common.filter_simple import FirstOrderFilter
|
||||
from openpilot.common.numpy_fast import interp
|
||||
from openpilot.selfdrive.car.interfaces import LatControlInputs
|
||||
from openpilot.selfdrive.controls.lib.drive_helpers import CONTROL_N
|
||||
from openpilot.selfdrive.modeld.constants import ModelConstants
|
||||
|
||||
from openpilot.frogpilot.common.frogpilot_variables import TORQUE_NN_MODEL_PATH, get_nnff_model_files, params
|
||||
from openpilot.frogpilot.common.frogpilot_variables import NNFF_MODELS_PATH, get_nnff_model_files, params
|
||||
|
||||
# At higher speeds (25+mph) we can assume:
|
||||
# Lateral acceleration achieved by a specific car correlates to
|
||||
@@ -121,7 +121,7 @@ def get_nn_model_path(car, eps_firmware) -> str | None:
|
||||
return None, 0.0
|
||||
|
||||
best = max(candidates, key=lambda model: similarity(model, query))
|
||||
return os.path.join(TORQUE_NN_MODEL_PATH, f"{best}.json"), similarity(best, query)
|
||||
return os.path.join(NNFF_MODELS_PATH, f"{best}.json"), similarity(best, query)
|
||||
|
||||
def find_valid_model(*queries):
|
||||
for query in queries:
|
||||
@@ -152,13 +152,19 @@ def sign(x):
|
||||
def similarity(s1: str, s2: str) -> float:
|
||||
return SequenceMatcher(None, s1, s2).ratio()
|
||||
|
||||
class LatControlInputs(NamedTuple):
|
||||
lateral_acceleration: float
|
||||
roll_compensation: float
|
||||
vego: float
|
||||
aego: float
|
||||
|
||||
class NeuralNetworkFeedforward:
|
||||
def __init__(self, CP, CI, LatControlTorque):
|
||||
def __init__(self, CP, LatControlTorque):
|
||||
self.lat_control_torque = LatControlTorque
|
||||
|
||||
self.lat_torque_nn_model = get_nn_model(CP.carFingerprint, str(next((fw.fwVersion for fw in CP.carFw if fw.ecu == "eps"), "")).replace("\\", ""))
|
||||
|
||||
self.torque_from_lateral_accel = CI.torque_from_lateral_accel()
|
||||
self.torque_from_lateral_accel = self.lat_control_torque.torque_from_lateral_accel
|
||||
|
||||
self.use_steering_angle = self.lat_control_torque.torque_params.useSteeringAngle
|
||||
|
||||
@@ -200,8 +206,6 @@ class NeuralNetworkFeedforward:
|
||||
self.past_future_len = len(self.past_times) + len(self.nn_future_times)
|
||||
self.roll_deque = deque(maxlen=history_check_frames[0])
|
||||
|
||||
self.nnLog = []
|
||||
|
||||
def update_live_delay(self, lateral_delay):
|
||||
self.lateral_delay = lateral_delay
|
||||
|
||||
@@ -285,25 +289,15 @@ class NeuralNetworkFeedforward:
|
||||
|
||||
# apply friction override for cars with low NN friction response
|
||||
if self.nn_friction_override:
|
||||
pid_log.error += self.torque_from_lateral_accel(LatControlInputs(0.0, 0.0, CS.vEgo, CS.aEgo), self.lat_control_torque.torque_params,
|
||||
friction_input, lateral_accel_deadzone, friction_compensation=True, gravity_adjusted=False)
|
||||
|
||||
self.nnLog = nn_input + nnff_setpoint_input + nnff_measurement_input
|
||||
pid_log.error += self.torque_from_lateral_accel(0.0, self.lat_control_torque.torque_params)
|
||||
else:
|
||||
torque_from_setpoint = self.torque_from_lateral_accel(LatControlInputs(setpoint, roll_compensation, CS.vEgo, CS.aEgo), self.lat_control_torque.torque_params,
|
||||
lateral_jerk_setpoint, lateral_accel_deadzone, friction_compensation=True, gravity_adjusted=False)
|
||||
torque_from_measurement = self.torque_from_lateral_accel(measurement, self.lat_control_torque.torque_params)
|
||||
torque_from_setpoint = self.torque_from_lateral_accel(setpoint, self.lat_control_torque.torque_params)
|
||||
|
||||
torque_from_measurement = self.torque_from_lateral_accel(LatControlInputs(measurement, roll_compensation, CS.vEgo, CS.aEgo), self.lat_control_torque.torque_params,
|
||||
lateral_jerk_measurement, lateral_accel_deadzone, friction_compensation=True, gravity_adjusted=False)
|
||||
|
||||
pid_log.error = torque_from_setpoint - torque_from_measurement
|
||||
pid_log.error = float(torque_from_setpoint - torque_from_measurement)
|
||||
|
||||
error = desired_lateral_accel - actual_lateral_accel
|
||||
friction_input = self.lat_accel_friction_factor * error + self.lat_jerk_friction_factor * lookahead_lateral_jerk
|
||||
ff = self.torque_from_lateral_accel(LatControlInputs(gravity_adjusted_lateral_accel, roll_compensation, CS.vEgo, CS.aEgo), self.lat_control_torque.torque_params,
|
||||
friction_input, lateral_accel_deadzone, friction_compensation=True,
|
||||
gravity_adjusted=True)
|
||||
ff = self.torque_from_lateral_accel(gravity_adjusted_lateral_accel, self.lat_control_torque.torque_params)
|
||||
|
||||
self.nnLog = []
|
||||
|
||||
return torque_from_setpoint, torque_from_measurement, pid_log, ff
|
||||
return pid_log, ff
|
||||
|
||||
@@ -7,11 +7,9 @@ import numpy as np
|
||||
import requests
|
||||
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from datetime import datetime
|
||||
|
||||
from openpilot.common.conversions import Conversions as CV
|
||||
from openpilot.common.realtime import DT_MDL
|
||||
from openpilot.common.time import system_time_valid
|
||||
|
||||
from openpilot.frogpilot.common.frogpilot_utilities import calculate_bearing_offset, calculate_distance_to_point, is_url_pingable
|
||||
from openpilot.frogpilot.common.frogpilot_variables import params, params_memory
|
||||
@@ -37,7 +35,6 @@ class SpeedLimitController:
|
||||
self.source = "None"
|
||||
|
||||
self.mapbox_requests = json.loads(params.get("MapBoxRequests") or "{}")
|
||||
self.mapbox_requests.setdefault("month", datetime.now().month)
|
||||
self.mapbox_requests.setdefault("total_requests", 0)
|
||||
self.mapbox_requests.setdefault("max_requests", FREE_MAPBOX_REQUESTS - (28 * 100))
|
||||
|
||||
@@ -80,7 +77,7 @@ class SpeedLimitController:
|
||||
]
|
||||
return next((offset for low, high, offset in offset_map if low < self.target < high), 0)
|
||||
|
||||
def get_mapbox_speed_limit(self, gps_position, v_ego, sm):
|
||||
def get_mapbox_speed_limit(self, gps_position, now, time_validated, v_ego, sm):
|
||||
if not gps_position or not self.mapbox_token or (sm["carState"].steeringAngleDeg - sm["liveParameters"].angleOffsetDeg) >= 45:
|
||||
self.mapbox_limit = 0
|
||||
self.segment_distance = 0
|
||||
@@ -107,13 +104,13 @@ class SpeedLimitController:
|
||||
self.segment_distance = 1000
|
||||
return None
|
||||
|
||||
if system_time_valid():
|
||||
current_month = datetime.now().month
|
||||
if time_validated:
|
||||
current_month = now.month
|
||||
if current_month != self.mapbox_requests.get("month"):
|
||||
self.mapbox_requests.update({
|
||||
"month": current_month,
|
||||
"total_requests": 0,
|
||||
"max_requests": FREE_MAPBOX_REQUESTS - calendar.monthrange(datetime.now().year, current_month)[1] * 100,
|
||||
"max_requests": FREE_MAPBOX_REQUESTS - calendar.monthrange(now.year, current_month)[1] * 100,
|
||||
})
|
||||
|
||||
self.mapbox_requests["total_requests"] += 1
|
||||
@@ -204,7 +201,7 @@ class SpeedLimitController:
|
||||
def handle_limit_change(self, desired_source, desired_target, sm):
|
||||
self.speed_limit_changed_timer += DT_MDL
|
||||
|
||||
speed_limit_accepted = (sm["frogpilotCarState"].accelPressed and not sm["carControl"].cruiseControl.override) or params_memory.get_bool("SpeedLimitAccepted")
|
||||
speed_limit_accepted = (sm["frogpilotCarState"].accelPressed and sm["carControl"].longActive) or params_memory.get_bool("SpeedLimitAccepted")
|
||||
speed_limit_denied = sm["frogpilotCarState"].decelPressed or (self.speed_limit_changed_timer >= 30)
|
||||
|
||||
if speed_limit_accepted:
|
||||
@@ -241,7 +238,7 @@ class SpeedLimitController:
|
||||
|
||||
params.put_float_nonblocking("PreviousSpeedLimit", self.target)
|
||||
|
||||
def update_limits(self, dashboard_speed_limit, gps_position, navigation_speed_limit, v_cruise, v_ego, sm):
|
||||
def update_limits(self, dashboard_speed_limit, gps_position, navigation_speed_limit, now, time_validated, v_cruise, v_ego, sm):
|
||||
self.update_map_speed_limit(gps_position, v_ego)
|
||||
|
||||
limits = {
|
||||
@@ -279,7 +276,7 @@ class SpeedLimitController:
|
||||
|
||||
if desired_target == 0 or self.target == 0:
|
||||
if self.mapbox_requests["total_requests"] < self.mapbox_requests["max_requests"] and self.frogpilot_toggles.slc_mapbox_filler:
|
||||
self.get_mapbox_speed_limit(gps_position, v_ego, sm)
|
||||
self.get_mapbox_speed_limit(gps_position, now, time_validated, v_ego, sm)
|
||||
|
||||
if self.mapbox_limit >= 1:
|
||||
desired_source = "Mapbox"
|
||||
@@ -307,23 +304,6 @@ class SpeedLimitController:
|
||||
self.speed_limit_changed_timer = 0
|
||||
self.unconfirmed_speed_limit = 0
|
||||
|
||||
def update_override(self, v_cruise, v_cruise_diff, v_ego, v_ego_diff, sm):
|
||||
self.override_slc = self.overridden_speed > self.target + self.offset > 0
|
||||
self.override_slc |= sm["carState"].gasPressed and v_ego > self.target + self.offset > 0
|
||||
self.override_slc &= sm["controlsState"].enabled
|
||||
|
||||
if self.override_slc:
|
||||
if self.frogpilot_toggles.speed_limit_controller_override_manual:
|
||||
if sm["carState"].gasPressed:
|
||||
self.overridden_speed = max(v_ego + v_ego_diff, self.overridden_speed)
|
||||
self.overridden_speed = float(np.clip(self.overridden_speed, self.target + self.offset, v_cruise + v_cruise_diff))
|
||||
elif self.frogpilot_toggles.speed_limit_controller_override_set_speed:
|
||||
self.overridden_speed = v_cruise + v_cruise_diff
|
||||
|
||||
self.source = "None"
|
||||
else:
|
||||
self.overridden_speed = 0
|
||||
|
||||
def update_map_speed_limit(self, gps_position, v_ego):
|
||||
if not gps_position:
|
||||
return
|
||||
@@ -351,3 +331,20 @@ class SpeedLimitController:
|
||||
|
||||
if distance_to_upcoming < max_lookahead:
|
||||
self.map_speed_limit = self.next_speed_limit
|
||||
|
||||
def update_override(self, v_cruise, v_cruise_diff, v_ego, v_ego_diff, sm):
|
||||
self.override_slc = self.overridden_speed > self.target + self.offset > 0
|
||||
self.override_slc |= sm["carState"].gasPressed and v_ego > self.target + self.offset > 0
|
||||
self.override_slc &= sm["controlsState"].enabled
|
||||
|
||||
if self.override_slc:
|
||||
if self.frogpilot_toggles.speed_limit_controller_override_manual:
|
||||
if sm["carState"].gasPressed:
|
||||
self.overridden_speed = max(v_ego + v_ego_diff, self.overridden_speed)
|
||||
self.overridden_speed = float(np.clip(self.overridden_speed, self.target + self.offset, v_cruise + v_cruise_diff))
|
||||
elif self.frogpilot_toggles.speed_limit_controller_override_set_speed:
|
||||
self.overridden_speed = v_cruise + v_cruise_diff
|
||||
|
||||
self.source = "None"
|
||||
else:
|
||||
self.overridden_speed = 0
|
||||
|
||||
@@ -10,8 +10,8 @@ from cereal import messaging
|
||||
from openpilot.common.realtime import DT_MDL, Priority, Ratekeeper, config_realtime_process
|
||||
from openpilot.common.time import system_time_valid
|
||||
|
||||
from openpilot.frogpilot.assets.model_manager import ModelManager, MODEL_DOWNLOAD_ALL_PARAM, MODEL_DOWNLOAD_PARAM
|
||||
from openpilot.frogpilot.assets.theme_manager import ThemeManager
|
||||
from openpilot.frogpilot.assets.model_manager import MODEL_DOWNLOAD_ALL_PARAM, MODEL_DOWNLOAD_PARAM, ModelManager
|
||||
from openpilot.frogpilot.assets.theme_manager import THEME_COMPONENT_PARAMS, ThemeManager
|
||||
from openpilot.frogpilot.common.frogpilot_functions import backup_toggles
|
||||
from openpilot.frogpilot.common.frogpilot_utilities import flash_panda, is_url_pingable, lock_doors, run_thread_with_lock, update_maps, update_openpilot
|
||||
from openpilot.frogpilot.common.frogpilot_variables import ERROR_LOGS_PATH, FrogPilotVariables, get_frogpilot_toggles, params, params_cache, params_memory
|
||||
@@ -21,9 +21,11 @@ from openpilot.frogpilot.system.frogpilot_stats import send_stats
|
||||
|
||||
ASSET_CHECK_RATE = (1 / DT_MDL)
|
||||
|
||||
def assets_checks(model_manager, theme_manager):
|
||||
def assets_checks(model_manager, theme_manager, frogpilot_toggles):
|
||||
if params_memory.get_bool(MODEL_DOWNLOAD_ALL_PARAM):
|
||||
run_thread_with_lock("download_all_models", model_manager.download_all_models)
|
||||
elif params_memory.get_bool("UpdateTinygrad"):
|
||||
run_thread_with_lock("update_tinygrad", model_manager.update_tinygrad)
|
||||
else:
|
||||
model_to_download = params_memory.get(MODEL_DOWNLOAD_PARAM, encoding="utf-8")
|
||||
if model_to_download:
|
||||
@@ -34,24 +36,15 @@ def assets_checks(model_manager, theme_manager):
|
||||
|
||||
report_data = json.loads(params_memory.get("IssueReported", encoding="utf-8") or "{}")
|
||||
if report_data:
|
||||
sentry.capture_report(report_data["DiscordUser"], report_data["Issue"], vars(get_frogpilot_toggles()))
|
||||
sentry.capture_report(report_data["DiscordUser"], report_data["Issue"], vars(frogpilot_toggles))
|
||||
params_memory.remove("IssueReported")
|
||||
|
||||
assets = [
|
||||
("ColorToDownload", "colors"),
|
||||
("DistanceIconToDownload", "distance_icons"),
|
||||
("IconToDownload", "icons"),
|
||||
("SignalToDownload", "signals"),
|
||||
("SoundToDownload", "sounds"),
|
||||
("WheelToDownload", "steering_wheels")
|
||||
]
|
||||
|
||||
for param, asset_type in assets:
|
||||
asset_to_download = params_memory.get(param, encoding="utf-8")
|
||||
for asset_type, asset_param in THEME_COMPONENT_PARAMS.items():
|
||||
asset_to_download = params_memory.get(asset_param, encoding="utf-8")
|
||||
if asset_to_download:
|
||||
run_thread_with_lock("download_theme", theme_manager.download_theme, (asset_type, asset_to_download, param))
|
||||
run_thread_with_lock("download_theme", theme_manager.download_theme, (asset_type, asset_to_download, asset_param, frogpilot_toggles))
|
||||
|
||||
def update_checks(manually_updated, model_manager, now, theme_manager, frogpilot_toggles, boot_run=False):
|
||||
def update_checks(model_manager, now, theme_manager, frogpilot_toggles, boot_run=False):
|
||||
while not (is_url_pingable("https://github.com") or is_url_pingable("https://gitlab.com")):
|
||||
time.sleep(60)
|
||||
|
||||
@@ -60,7 +53,7 @@ def update_checks(manually_updated, model_manager, now, theme_manager, frogpilot
|
||||
|
||||
run_thread_with_lock("update_maps", update_maps, (now,))
|
||||
|
||||
if frogpilot_toggles.automatic_updates and not manually_updated:
|
||||
if frogpilot_toggles.automatic_updates:
|
||||
run_thread_with_lock("update_openpilot", update_openpilot)
|
||||
|
||||
time.sleep(1)
|
||||
@@ -80,14 +73,14 @@ def frogpilot_thread():
|
||||
model_manager = ModelManager()
|
||||
theme_manager = ThemeManager()
|
||||
|
||||
toggles_last_updated = datetime.datetime.now()
|
||||
toggles_last_updated = datetime.datetime.now(datetime.timezone.utc)
|
||||
|
||||
pm = messaging.PubMaster(["frogpilotPlan"])
|
||||
sm = messaging.SubMaster(["carControl", "carState", "controlsState", "deviceState", "driverMonitoringState",
|
||||
"liveLocationKalman", "liveParameters", "managerState", "modelV2",
|
||||
"pandaStates", "radarState", "frogpilotCarState",
|
||||
"frogpilotNavigation"],
|
||||
poll="modelV2", ignore_avg_freq=["radarState"])
|
||||
"liveLocationKalman", "liveParameters", "managerState", "modelV2", "onroadEvents",
|
||||
"pandaStates", "radarState", "frogpilotCarState", "frogpilotControlsState",
|
||||
"frogpilotModelV2", "frogpilotNavigation", "frogpilotOnroadEvents"],
|
||||
poll="modelV2", ignore_avg_freq=["frogpilotRadarState"])
|
||||
|
||||
run_update_checks = False
|
||||
started_previously = False
|
||||
@@ -97,7 +90,7 @@ def frogpilot_thread():
|
||||
while True:
|
||||
sm.update()
|
||||
|
||||
now = datetime.datetime.now()
|
||||
now = datetime.datetime.now(datetime.timezone.utc)
|
||||
|
||||
started = sm["deviceState"].started
|
||||
|
||||
@@ -116,32 +109,28 @@ def frogpilot_thread():
|
||||
if time_validated and is_url_pingable(os.environ.get("STATS_URL", "")):
|
||||
send_stats()
|
||||
|
||||
params_memory.put_bool("IsOnroad", False)
|
||||
|
||||
elif started and not started_previously:
|
||||
frogpilot_planner = FrogPilotPlanner()
|
||||
frogpilot_tracking = FrogPilotTracking()
|
||||
|
||||
if error_log.is_file():
|
||||
error_log.unlink()
|
||||
|
||||
params_memory.put_bool("IsOnroad", True)
|
||||
frogpilot_planner = FrogPilotPlanner(error_log, theme_manager)
|
||||
frogpilot_tracking = FrogPilotTracking(frogpilot_planner, frogpilot_toggles)
|
||||
|
||||
if started and sm.updated["modelV2"]:
|
||||
frogpilot_planner.update(sm, frogpilot_toggles)
|
||||
frogpilot_planner.publish(sm, pm, theme_manager.theme_updated, toggles_updated)
|
||||
frogpilot_planner.update(now, time_validated, sm, frogpilot_toggles)
|
||||
frogpilot_planner.publish(theme_manager.theme_updated, toggles_updated, sm, pm, frogpilot_toggles)
|
||||
|
||||
frogpilot_tracking.update(sm)
|
||||
elif not started and toggles_updated:
|
||||
frogpilot_tracking.update(now, time_validated, sm, frogpilot_toggles)
|
||||
elif not started:
|
||||
frogpilot_plan_send = messaging.new_message("frogpilotPlan")
|
||||
frogpilot_plan_send.frogpilotPlan.themeUpdated = theme_manager.theme_updated
|
||||
frogpilot_plan_send.frogpilotPlan.themeUpdated = theme_manager.theme_updated or params_memory.get_bool("UseActiveTheme")
|
||||
frogpilot_plan_send.frogpilotPlan.togglesUpdated = toggles_updated
|
||||
pm.send("frogpilotPlan", frogpilot_plan_send)
|
||||
|
||||
started_previously = started
|
||||
|
||||
if rate_keeper.frame % ASSET_CHECK_RATE == 0:
|
||||
assets_checks(model_manager, theme_manager)
|
||||
assets_checks(model_manager, theme_manager, frogpilot_toggles)
|
||||
|
||||
if params_memory.get_bool("FrogPilotTogglesUpdated") or theme_manager.theme_updated:
|
||||
previous_holiday_themes = frogpilot_toggles.holiday_themes
|
||||
@@ -163,15 +152,13 @@ def frogpilot_thread():
|
||||
|
||||
toggles_updated = (now - toggles_last_updated).total_seconds() <= 1
|
||||
|
||||
manually_updated = params_memory.get_bool("ManualUpdateInitiated")
|
||||
|
||||
run_update_checks |= manually_updated
|
||||
run_update_checks |= params_memory.get_bool("ManualUpdateInitiated")
|
||||
run_update_checks |= now.second == 0 and (now.minute % 60 == 0 or (now.minute % 5 == 0 and frogpilot_toggles.frogs_go_moo))
|
||||
run_update_checks &= time_validated
|
||||
|
||||
if run_update_checks:
|
||||
theme_manager.update_active_theme(time_validated, frogpilot_toggles)
|
||||
run_thread_with_lock("update_checks", update_checks, (manually_updated, model_manager, now, theme_manager, frogpilot_toggles))
|
||||
run_thread_with_lock("update_checks", update_checks, (model_manager, now, theme_manager, frogpilot_toggles))
|
||||
|
||||
run_update_checks = False
|
||||
elif not time_validated:
|
||||
@@ -180,7 +167,7 @@ def frogpilot_thread():
|
||||
continue
|
||||
|
||||
theme_manager.update_active_theme(time_validated, frogpilot_toggles)
|
||||
run_thread_with_lock("update_checks", update_checks, (manually_updated, model_manager, now, theme_manager, frogpilot_toggles, True))
|
||||
run_thread_with_lock("update_checks", update_checks, (model_manager, now, theme_manager, frogpilot_toggles, True))
|
||||
|
||||
rate_keeper.keep_time()
|
||||
|
||||
|
||||
@@ -2,18 +2,21 @@ import json
|
||||
import os
|
||||
import random
|
||||
import requests
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
sys.path.append(os.path.join(os.path.dirname(__file__), "..", "third_party"))
|
||||
|
||||
from collections import Counter
|
||||
from datetime import datetime, timezone
|
||||
from influxdb_client import InfluxDBClient, Point
|
||||
from influxdb_client.client.write_api import SYNCHRONOUS
|
||||
|
||||
from openpilot.common.conversions import Conversions as CV
|
||||
from openpilot.system.hardware import HARDWARE
|
||||
from openpilot.system.version import get_build_metadata
|
||||
|
||||
from openpilot.frogpilot.common.frogpilot_utilities import run_cmd
|
||||
from openpilot.frogpilot.common.frogpilot_variables import get_frogpilot_toggles, params, params_tracking
|
||||
from openpilot.frogpilot.common.frogpilot_utilities import clean_model_name, run_cmd
|
||||
from openpilot.frogpilot.common.frogpilot_variables import get_frogpilot_toggles, params
|
||||
|
||||
BASE_URL = "https://nominatim.openstreetmap.org"
|
||||
MINIMUM_POPULATION = 100_000
|
||||
@@ -94,26 +97,6 @@ def get_city_center(latitude, longitude):
|
||||
print(f"Falling back to (0, 0) for {latitude}, {longitude}")
|
||||
return float(0.0), float(0.0), "N/A", "N/A", "N/A"
|
||||
|
||||
def install_influxdb_client():
|
||||
try:
|
||||
import influxdb_client
|
||||
import influxdb_client.client.write_api
|
||||
except ModuleNotFoundError:
|
||||
print("influxdb-client not found. Attempting installation...")
|
||||
stock_mount_options = subprocess.run(["findmnt", "-no", "OPTIONS", "/"], capture_output=True, text=True, check=True).stdout.strip()
|
||||
|
||||
run_cmd(["sudo", "mount", "-o", "remount,rw", "/"], "Successfully remounted / as read-write", "Failed to remount / as read-write", report=False)
|
||||
run_cmd(["sudo", sys.executable, "-m", "pip", "install", "influxdb-client"], "Successfully installed influxdb-client", "Failed to install influxdb-client", report=False)
|
||||
run_cmd(["sudo", "mount", "-o", f"remount,{stock_mount_options}", "/"], "Successfully restored stock mount options", "Failed to restore stock mount options", report=False)
|
||||
|
||||
def is_up_to_date(build_metadata):
|
||||
remote_commit = subprocess.check_output(["git", "ls-remote", "origin", build_metadata.channel], text=True, stderr=subprocess.DEVNULL).strip()
|
||||
|
||||
if remote_commit:
|
||||
return build_metadata.openpilot.git_commit == remote_commit.split()[0]
|
||||
|
||||
return True
|
||||
|
||||
def send_stats():
|
||||
try:
|
||||
build_metadata = get_build_metadata()
|
||||
@@ -125,11 +108,6 @@ def send_stats():
|
||||
if frogpilot_toggles.car_make == "mock":
|
||||
return
|
||||
|
||||
install_influxdb_client()
|
||||
|
||||
from influxdb_client import InfluxDBClient, Point
|
||||
from influxdb_client.client.write_api import SYNCHRONOUS
|
||||
|
||||
bucket = os.environ.get("STATS_BUCKET", "")
|
||||
org_ID = os.environ.get("STATS_ORG_ID", "")
|
||||
token = os.environ.get("STATS_TOKEN", "")
|
||||
@@ -159,16 +137,19 @@ def send_stats():
|
||||
selected_theme = random.choice([item for item, count in most_common if count == max_count]).replace("-user_created", "").replace("_", " ")
|
||||
|
||||
point = (Point("user_stats")
|
||||
.field("blocked_user", frogpilot_toggles.block_user)
|
||||
.field("car_make", "GM" if frogpilot_toggles.car_make == "gm" else frogpilot_toggles.car_make.title())
|
||||
.field("car_model", frogpilot_toggles.car_model)
|
||||
.field("city", city)
|
||||
.field("commit", build_metadata.openpilot.git_commit)
|
||||
.field("country", country)
|
||||
.field("current_months_kilometers", int(frogpilot_stats.get("CurrentMonthsKilometers", 0)))
|
||||
.field("device", HARDWARE.get_device_type())
|
||||
.field("driving_model", frogpilot_toggles.model_name.replace("🗺️", "").replace("📡", "").replace("👀", "").replace("(Default)", "").strip())
|
||||
.field("driving_model", clean_model_name(frogpilot_toggles.model_name))
|
||||
.field("event", 1)
|
||||
.field("frogpilot_drives", params_tracking.get_int("FrogPilotDrives"))
|
||||
.field("frogpilot_hours", params_tracking.get_int("FrogPilotMinutes") / 60)
|
||||
.field("frogpilot_miles", params_tracking.get_int("FrogPilotKilometers") * CV.KPH_TO_MPH)
|
||||
.field("frogpilot_drives", int(frogpilot_stats.get("FrogPilotDrives", 0)))
|
||||
.field("frogpilot_hours", float(frogpilot_stats.get("FrogPilotSeconds", 0)) / (60 * 60))
|
||||
.field("frogpilot_miles", float(frogpilot_stats.get("FrogPilotMeters", 0)) * CV.METER_TO_MILE)
|
||||
.field("goat_scream", frogpilot_toggles.goat_scream_alert)
|
||||
.field("has_cc_long", frogpilot_toggles.has_cc_long)
|
||||
.field("has_openpilot_longitudinal", frogpilot_toggles.openpilot_longitudinal)
|
||||
@@ -181,12 +162,11 @@ def send_stats():
|
||||
.field("random_events", frogpilot_toggles.random_events)
|
||||
.field("state", state)
|
||||
.field("theme", selected_theme.title())
|
||||
.field("total_aol_seconds", float(frogpilot_stats.get("TotalAOLTime", 0)))
|
||||
.field("total_lateral_seconds", float(frogpilot_stats.get("TotalLateralTime", 0)))
|
||||
.field("total_longitudinal_seconds", float(frogpilot_stats.get("TotalLongitudinalTime", 0)))
|
||||
.field("total_tracked_seconds", float(frogpilot_stats.get("TotalTrackedTime", 0)))
|
||||
.field("total_aol_seconds", float(frogpilot_stats.get("AOLTime", 0)))
|
||||
.field("total_lateral_seconds", float(frogpilot_stats.get("LateralTime", 0)))
|
||||
.field("total_longitudinal_seconds", float(frogpilot_stats.get("LongitudinalTime", 0)))
|
||||
.field("total_tracked_seconds", float(frogpilot_stats.get("TrackedTime", 0)))
|
||||
.field("tuning_level", params.get_int("TuningLevel") + 1 if params.get_bool("TuningLevelConfirmed") else 0)
|
||||
.field("up_to_date", is_up_to_date(build_metadata))
|
||||
.field("using_stock_acc", not (frogpilot_toggles.has_cc_long or frogpilot_toggles.openpilot_longitudinal))
|
||||
|
||||
.tag("branch", build_metadata.channel)
|
||||
|
||||
@@ -7,8 +7,6 @@ import time
|
||||
from collections import OrderedDict, deque
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
import openpilot.system.sentry as sentry
|
||||
|
||||
from cereal import log, messaging
|
||||
|
||||
from openpilot.frogpilot.common.frogpilot_utilities import calculate_distance_to_point, calculate_lane_width, is_url_pingable
|
||||
@@ -126,7 +124,7 @@ class MapSpeedLogger:
|
||||
if self.should_stop_processing:
|
||||
return False
|
||||
|
||||
time.sleep(10)
|
||||
time.sleep(5)
|
||||
return True
|
||||
|
||||
def fetch_from_overpass(self, latitude, longitude):
|
||||
@@ -379,35 +377,29 @@ def main():
|
||||
previously_started = False
|
||||
|
||||
while True:
|
||||
try:
|
||||
logger.sm.update()
|
||||
logger.sm.update()
|
||||
|
||||
if logger.sm["deviceState"].started:
|
||||
logger.log_speed_limit()
|
||||
if logger.sm["deviceState"].started:
|
||||
logger.log_speed_limit()
|
||||
|
||||
previously_started = True
|
||||
elif previously_started:
|
||||
existing_dataset = json.loads(params.get("SpeedLimits") or "[]")
|
||||
existing_dataset.extend(logger.dataset_additions)
|
||||
previously_started = True
|
||||
elif previously_started:
|
||||
existing_dataset = json.loads(params.get("SpeedLimits") or "[]")
|
||||
existing_dataset.extend(logger.dataset_additions)
|
||||
|
||||
new_dataset = logger.cleanup_dataset(existing_dataset)
|
||||
params.put("SpeedLimits", json.dumps(list(new_dataset)))
|
||||
new_dataset = logger.cleanup_dataset(existing_dataset)
|
||||
params.put("SpeedLimits", json.dumps(list(new_dataset)))
|
||||
|
||||
if logger.sm["deviceState"].networkType in (NetworkType.ethernet, NetworkType.wifi):
|
||||
params_memory.put_bool("UpdateSpeedLimits", True)
|
||||
if logger.sm["deviceState"].networkType in (NetworkType.ethernet, NetworkType.wifi):
|
||||
params_memory.put_bool("UpdateSpeedLimits", True)
|
||||
|
||||
logger.dataset_additions.clear()
|
||||
logger.dataset_additions.clear()
|
||||
|
||||
previously_started = False
|
||||
elif params_memory.get_bool("UpdateSpeedLimits"):
|
||||
logger.process_speed_limits()
|
||||
else:
|
||||
time.sleep(5)
|
||||
|
||||
except Exception as exception:
|
||||
print(f"Error in speed_limit_filler: {exception}")
|
||||
sentry.capture_exception(exception)
|
||||
time.sleep(1)
|
||||
previously_started = False
|
||||
elif params_memory.get_bool("UpdateSpeedLimits"):
|
||||
logger.process_speed_limits()
|
||||
else:
|
||||
time.sleep(5)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
@@ -782,7 +782,7 @@ function NavigationDestination({
|
||||
if (fav) {
|
||||
removeFavorite(fav);
|
||||
} else {
|
||||
showSnackbar("Couldn’t find favorite entry…");
|
||||
showSnackbar("Couldn't find favorite entry…");
|
||||
}
|
||||
} else {
|
||||
await favoriteDestination();
|
||||
|
||||
@@ -227,7 +227,7 @@ async function openOverlay(route) {
|
||||
|
||||
downloadButton.onclick = () => {
|
||||
const link = document.createElement("a");
|
||||
const videoPath = `/video/${route.name}--${current}?camera=${selectedCamera}`;
|
||||
const videoPath = `/video/${route.name}/combined?camera=${selectedCamera}`;
|
||||
link.href = videoPath;
|
||||
link.download = `${route.timestamp}-${selectedCamera}.mp4`;
|
||||
document.body.appendChild(link);
|
||||
@@ -251,8 +251,7 @@ async function openOverlay(route) {
|
||||
vid.load();
|
||||
vid.play();
|
||||
} catch (error) {
|
||||
showSnackbar("Error: Could not load all route segments.", "error");
|
||||
segments = [`/video/${route.name}--0`];
|
||||
showSnackbar("Error: Could not load combined route video.", "error");
|
||||
}
|
||||
})();
|
||||
|
||||
@@ -271,8 +270,7 @@ async function openOverlay(route) {
|
||||
overlay.querySelectorAll(".camera-button").forEach(btn => btn.classList.remove("active"));
|
||||
e.target.classList.add("active");
|
||||
selectedCamera = e.target.dataset.camera;
|
||||
const videoPath = segments[current].includes("?") ? `${segments[current]}&camera=${selectedCamera}` : `${segments[current]}?camera=${selectedCamera}`
|
||||
vid.src = videoPath;
|
||||
vid.src = segments[current].includes("?") ? `${segments[current]}&camera=${selectedCamera}` : `${segments[current]}?camera=${selectedCamera}`;
|
||||
vid.load();
|
||||
vid.play();
|
||||
});
|
||||
|
||||
@@ -12,6 +12,7 @@ import { ScreenRecordings } from "/assets/components/recordings/screen_recording
|
||||
import { Sidebar } from "/assets/components/sidebar.js"
|
||||
import { SpeedLimits } from "/assets/components/tools/speed_limits.js"
|
||||
import { TailscaleControl } from "/assets/components/tailscale/tailscale.js"
|
||||
import { ThemeMaker } from "/assets/components/tools/theme_maker.js"
|
||||
import { TmuxLog } from "/assets/components/tools/tmux.js"
|
||||
import { ToggleControl } from "/assets/components/tools/toggles.js"
|
||||
import { TSKManager } from "/assets/components/tools/tsk_manager.js"
|
||||
@@ -39,6 +40,7 @@ function Root() {
|
||||
createRoute("settings", "/settings/:section/:subsection?", SettingsView),
|
||||
createRoute("speed_limits", "/download_speed_limits", SpeedLimits),
|
||||
createRoute("tailscale", "/manage_tailscale", TailscaleControl),
|
||||
createRoute("thememaker", "/theme_maker", ThemeMaker),
|
||||
createRoute("tmux", "/manage_tmux", TmuxLog),
|
||||
createRoute("toggles", "/manage_toggles", ToggleControl),
|
||||
createRoute("tsk_manager", "/tsk_manager", TSKManager),
|
||||
|
||||
@@ -21,6 +21,7 @@ const MenuItems = {
|
||||
{ name: "Download Speed Limits", link: "/download_speed_limits", icon: "bi-download" },
|
||||
{ name: "Error Logs", link: "/manage_error_logs", icon: "bi-exclamation-triangle" },
|
||||
{ name: "Lock/Unlock Doors", link: "/lock_or_unlock_doors", icon: "bi-door-closed" },
|
||||
{ name: "Theme Maker", link: "/theme_maker", icon: "bi-palette-fill" },
|
||||
{ name: "Tmux Log", link: "/manage_tmux", icon: "bi-terminal" },
|
||||
{ name: "Toggles", link: "/manage_toggles", icon: "bi-toggle-on" },
|
||||
{ name: "Toyota Security Keys", link: "/tsk_manager", icon: "bi-key-fill" },
|
||||
|
||||
@@ -0,0 +1,720 @@
|
||||
.apply-button {
|
||||
background-color: var(--accent-bg);
|
||||
border: none;
|
||||
border-radius: var(--border-radius-lg);
|
||||
color: var(--text-color);
|
||||
cursor: pointer;
|
||||
font-size: var(--font-size-base);
|
||||
font-weight: var(--font-weight-bold);
|
||||
overflow: hidden;
|
||||
padding: 0.75rem 1.5rem;
|
||||
transition: background-color var(--transition-fast), box-shadow var(--transition-fast), transform var(--transition-fast);
|
||||
white-space: nowrap;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.apply-button:hover {
|
||||
background-color: var(--accent-hover-bg);
|
||||
box-shadow: var(--shadow-md);
|
||||
transform: var(--hover-scale-sm);
|
||||
}
|
||||
|
||||
.checklist-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--gap-sm);
|
||||
margin-top: var(--margin-base);
|
||||
}
|
||||
|
||||
.checklist-item {
|
||||
align-items: center;
|
||||
background-color: var(--input-bg);
|
||||
border-radius: var(--border-radius-md);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
overflow: hidden;
|
||||
padding: var(--padding-sm);
|
||||
transition: background-color var(--transition-fast), box-shadow var(--transition-fast), transform var(--transition-fast);
|
||||
}
|
||||
|
||||
.checklist-item .custom-checkbox {
|
||||
background-color: var(--color-gray-600);
|
||||
border-radius: 13px;
|
||||
flex-shrink: 0;
|
||||
height: 26px;
|
||||
position: relative;
|
||||
transition: background-color var(--transition-fast);
|
||||
width: 50px;
|
||||
}
|
||||
|
||||
.checklist-item .custom-checkbox::before {
|
||||
background-color: var(--color-white);
|
||||
border-radius: 50%;
|
||||
content: "";
|
||||
height: 20px;
|
||||
left: 4px;
|
||||
position: absolute;
|
||||
top: 3px;
|
||||
transition: transform var(--transition-fast);
|
||||
width: 20px;
|
||||
}
|
||||
|
||||
.checklist-item .label-text {
|
||||
color: var(--text-color);
|
||||
flex-grow: 1;
|
||||
font-weight: var(--font-weight-demi-bold);
|
||||
padding-left: var(--padding-sm);
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.checklist-item:hover {
|
||||
background-color: var(--main-fg);
|
||||
box-shadow: var(--shadow-sm);
|
||||
transform: var(--hover-scale-sm);
|
||||
}
|
||||
|
||||
.checklist-item input[type="checkbox"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.checklist-item input[type="checkbox"]:checked ~ .custom-checkbox {
|
||||
background-color: var(--success-bg);
|
||||
}
|
||||
|
||||
.checklist-item input[type="checkbox"]:checked ~ .custom-checkbox::before {
|
||||
transform: translateX(24px);
|
||||
}
|
||||
|
||||
.color-label input[type="color"] {
|
||||
appearance: none;
|
||||
background-color: transparent;
|
||||
border: none;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
height: 40px;
|
||||
overflow: hidden;
|
||||
transition: box-shadow var(--transition-fast);
|
||||
width: 40px;
|
||||
}
|
||||
|
||||
.color-label input[type="color"]::-moz-color-swatch {
|
||||
border: var(--border-width-base) solid var(--text-color);
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.color-label input[type="color"]::-webkit-color-swatch {
|
||||
border: var(--border-width-base) solid var(--text-color);
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.color-label:hover input[type="color"] {
|
||||
box-shadow: 0 0 8px var(--thumb-color);
|
||||
}
|
||||
|
||||
.color-section,
|
||||
.upload-section {
|
||||
display: grid;
|
||||
gap: var(--gap-md);
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.delete-theme-button {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--danger-fg);
|
||||
cursor: pointer;
|
||||
font-size: 1.2rem;
|
||||
padding: 0 0.5rem;
|
||||
transition: color var(--transition-fast), transform 0.2s;
|
||||
}
|
||||
|
||||
.delete-theme-button:hover {
|
||||
color: var(--danger-hover-bg);
|
||||
transform: scale(1.2);
|
||||
}
|
||||
|
||||
.discord-username-input {
|
||||
background-color: var(--input-bg);
|
||||
border: var(--border-style-input);
|
||||
border-radius: var(--border-radius-base);
|
||||
box-sizing: border-box;
|
||||
color: var(--text-color);
|
||||
margin-top: var(--border-radius-lg);
|
||||
padding: var(--padding-sm);
|
||||
transition: border-color var(--transition-fast), box-shadow var(--transition-fast), transform var(--transition-fast);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.download-theme-button {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--color-confirm);
|
||||
cursor: pointer;
|
||||
font-size: 1.2rem;
|
||||
padding: 0 0.5rem;
|
||||
transition: color var(--transition-fast), transform 0.2s;
|
||||
}
|
||||
|
||||
.download-theme-button:hover {
|
||||
color: var(--color-confirm-hover);
|
||||
transform: scale(1.2);
|
||||
}
|
||||
|
||||
.file-clear-button {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--danger-fg);
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
padding: 0 0.4rem;
|
||||
flex-shrink: 0;
|
||||
transition: color var(--transition-fast), transform 0.2s;
|
||||
}
|
||||
.file-clear-button:hover {
|
||||
color: var(--danger-hover-bg);
|
||||
transform: scale(1.1);
|
||||
}
|
||||
|
||||
.file-name-display {
|
||||
color: var(--text-muted);
|
||||
flex-grow: 1;
|
||||
font-size: 0.9em;
|
||||
font-style: italic;
|
||||
margin-right: var(--margin-sm);
|
||||
min-width: 0;
|
||||
overflow: hidden;
|
||||
text-align: right;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
label.file-upload-button {
|
||||
background-color: var(--success-bg);
|
||||
border-radius: var(--border-radius-sm);
|
||||
color: var(--text-color);
|
||||
cursor: pointer;
|
||||
flex-shrink: 0;
|
||||
font-size: 0.9em;
|
||||
padding: 0.3rem 0.6rem;
|
||||
transition: background-color 0.2s, color 0.2s;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.file-upload-input {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.file-upload-label {
|
||||
align-items: center;
|
||||
background-color: var(--secondary-bg);
|
||||
border-radius: var(--border-radius-md);
|
||||
display: flex;
|
||||
min-height: 2.5rem;
|
||||
overflow: hidden;
|
||||
padding: var(--padding-sm);
|
||||
transition: background-color 0.2s, box-shadow var(--transition-fast), transform var(--transition-fast);
|
||||
}
|
||||
|
||||
.file-upload-label:hover {
|
||||
background-color: var(--main-fg);
|
||||
box-shadow: var(--shadow-sm);
|
||||
transform: var(--hover-scale-sm);
|
||||
}
|
||||
|
||||
.file-upload-label:hover .file-upload-button {
|
||||
background-color: var(--sidebar-bg);
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.file-upload-text {
|
||||
color: var(--text-color);
|
||||
flex-shrink: 0;
|
||||
font-weight: var(--font-weight-demi-bold);
|
||||
margin-right: var(--margin-sm);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.help-icon {
|
||||
background-color: var(--main-fg);
|
||||
border-radius: 50%;
|
||||
color: var(--text-color);
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
font-weight: bold;
|
||||
height: 1.2em;
|
||||
line-height: 1.2em;
|
||||
margin-left: 0.5em;
|
||||
text-align: center;
|
||||
transition: background-color var(--transition-fast), transform var(--transition-fast);
|
||||
width: 1.2em;
|
||||
}
|
||||
|
||||
.help-icon:hover {
|
||||
background-color: var(--success-hover-bg);
|
||||
transform: var(--hover-scale-sm);
|
||||
}
|
||||
|
||||
.manage-themes-button {
|
||||
background-color: var(--color-confirm);
|
||||
border: none;
|
||||
border-radius: var(--border-radius-lg);
|
||||
color: var(--text-color);
|
||||
cursor: pointer;
|
||||
font-size: var(--font-size-base);
|
||||
font-weight: var(--font-weight-bold);
|
||||
overflow: hidden;
|
||||
padding: 0.75rem 1.5rem;
|
||||
transition: background-color var(--transition-fast), box-shadow var(--transition-fast), transform var(--transition-fast);
|
||||
white-space: nowrap;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.manage-themes-button:hover {
|
||||
background-color: var(--color-confirm-hover);
|
||||
box-shadow: var(--shadow-md);
|
||||
transform: var(--hover-scale-sm);
|
||||
}
|
||||
|
||||
.manage-themes-modal {
|
||||
animation: modalFadeIn 0.3s ease;
|
||||
max-width: var(--width-xl);
|
||||
overflow-x: hidden;
|
||||
}
|
||||
|
||||
.manage-themes-tabs {
|
||||
border: 1px solid var(--sidebar-border-color);
|
||||
border-radius: var(--border-radius-md);
|
||||
display: flex;
|
||||
margin-bottom: 1rem;
|
||||
overflow-x: hidden;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
|
||||
.save-button {
|
||||
background-color: var(--success-bg);
|
||||
border: none;
|
||||
border-radius: var(--border-radius-lg);
|
||||
color: var(--text-color);
|
||||
cursor: pointer;
|
||||
font-size: var(--font-size-base);
|
||||
font-weight: var(--font-weight-bold);
|
||||
overflow: hidden;
|
||||
padding: 0.75rem 1.5rem;
|
||||
transition: background-color var(--transition-fast), box-shadow var(--transition-fast), transform var(--transition-fast);
|
||||
white-space: nowrap;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.save-button:hover {
|
||||
background-color: var(--success-hover-bg);
|
||||
box-shadow: var(--shadow-md);
|
||||
transform: var(--hover-scale-sm);
|
||||
}
|
||||
|
||||
.save-button-wrapper {
|
||||
display: flex;
|
||||
gap: var(--gap-md);
|
||||
justify-content: center;
|
||||
margin-top: var(--margin-lg);
|
||||
}
|
||||
|
||||
.signal-type-toggle {
|
||||
display: flex;
|
||||
gap: var(--gap-sm);
|
||||
}
|
||||
|
||||
.submit-button {
|
||||
background-color: var(--accent-bg);
|
||||
border: none;
|
||||
border-radius: var(--border-radius-lg);
|
||||
color: var(--text-color);
|
||||
cursor: pointer;
|
||||
font-size: var(--font-size-base);
|
||||
font-weight: var(--font-weight-bold);
|
||||
overflow: hidden;
|
||||
padding: 0.75rem 1.5rem;
|
||||
transition: background-color var(--transition-fast), box-shadow var(--transition-fast), transform var(--transition-fast);
|
||||
white-space: nowrap;
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.submit-button:hover {
|
||||
background-color: var(--accent-hover-bg);
|
||||
box-shadow: var(--shadow-md);
|
||||
transform: var(--hover-scale-sm);
|
||||
}
|
||||
|
||||
.tab-button {
|
||||
background-color: var(--input-bg);
|
||||
border: none;
|
||||
color: var(--text-color);
|
||||
cursor: pointer;
|
||||
flex: 1;
|
||||
font-weight: var(--font-weight-demi-bold);
|
||||
padding: 0.75rem;
|
||||
transition: background-color var(--transition-fast), box-shadow var(--transition-fast), color var(--transition-fast), transform var(--transition-fast);
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.tab-button.active {
|
||||
background-color: var(--main-fg);
|
||||
color: white;
|
||||
}
|
||||
|
||||
.tab-button:not(:last-child) {
|
||||
border-right: 1px solid var(--sidebar-border-color);
|
||||
}
|
||||
|
||||
.tab-button:not(.active):hover {
|
||||
background-color: var(--main-fg);
|
||||
box-shadow: var(--shadow-sm);
|
||||
transform: var(--hover-scale-sm);
|
||||
}
|
||||
|
||||
.theme-button {
|
||||
background: none;
|
||||
border: none;
|
||||
color: var(--text-color);
|
||||
cursor: pointer;
|
||||
flex-grow: 1;
|
||||
font-weight: var(--font-weight-demi-bold);
|
||||
text-align: left;
|
||||
transition: color var(--transition-fast);
|
||||
}
|
||||
|
||||
.theme-button:hover {
|
||||
color: var(--text-color);
|
||||
}
|
||||
|
||||
.theme-item {
|
||||
align-items: center;
|
||||
background-color: var(--input-bg);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 0.75rem;
|
||||
transition: background-color var(--transition-fast), box-shadow var(--transition-fast), transform var(--transition-fast);
|
||||
}
|
||||
|
||||
.theme-item:not(:last-child) {
|
||||
border-bottom: 1px solid var(--sidebar-border-color);
|
||||
}
|
||||
|
||||
.theme-item:hover {
|
||||
background-color: var(--main-fg);
|
||||
box-shadow: var(--shadow-sm);
|
||||
transform: var(--hover-scale-sm);
|
||||
}
|
||||
|
||||
.theme-maker-container {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.theme-maker-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--gap-md);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.theme-maker-form .color-label {
|
||||
align-items: center;
|
||||
background-color: var(--secondary-bg);
|
||||
border-radius: var(--border-radius-md);
|
||||
color: var(--text-color);
|
||||
cursor: pointer;
|
||||
display: flex;
|
||||
font-weight: var(--font-weight-demi-bold);
|
||||
justify-content: space-between;
|
||||
padding: var(--padding-sm);
|
||||
transition: box-shadow var(--transition-fast), transform var(--transition-fast);
|
||||
}
|
||||
|
||||
.theme-maker-form .color-label:hover {
|
||||
box-shadow: var(--shadow-sm);
|
||||
transform: var(--hover-scale-sm);
|
||||
}
|
||||
|
||||
.theme-maker-main-title {
|
||||
background-color: var(--input-bg);
|
||||
border-radius: var(--border-radius-lg);
|
||||
box-shadow: var(--shadow-sm);
|
||||
box-sizing: border-box;
|
||||
color: var(--text-color);
|
||||
font-size: var(--font-size-lg);
|
||||
font-weight: var(--font-weight-bold);
|
||||
margin-bottom: var(--margin-base);
|
||||
padding: var(--padding-sm);
|
||||
text-align: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.theme-maker-main-widget {
|
||||
background-color: var(--secondary-bg);
|
||||
border-radius: var(--border-radius-lg);
|
||||
box-shadow: var(--shadow-md);
|
||||
color: var(--main-fg);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-top: var(--padding-xl);
|
||||
max-width: var(--width-xxxxl);
|
||||
padding: var(--padding-lg);
|
||||
transition: box-shadow var(--transition-fast), transform var(--transition-fast);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.theme-maker-main-widget:hover {
|
||||
transform: var(--hover-scale-sm);
|
||||
}
|
||||
|
||||
.theme-maker-sub-widgets {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--gap-lg);
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.theme-maker-title {
|
||||
background-color: var(--main-fg);
|
||||
border-radius: var(--border-radius-md);
|
||||
color: var(--text-color);
|
||||
font-size: var(--font-size-lg);
|
||||
font-weight: var(--font-weight-bold);
|
||||
margin: 0 auto var(--margin-base);
|
||||
padding: var(--padding-sm) var(--padding-lg);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.theme-maker-widget {
|
||||
background-color: var(--input-bg);
|
||||
border-radius: var(--border-radius-md);
|
||||
flex: 1 1 var(--width-md);
|
||||
overflow: visible;
|
||||
padding: var(--padding-base);
|
||||
transition: box-shadow var(--transition-fast), transform var(--transition-fast);
|
||||
}
|
||||
|
||||
.theme-maker-widget:hover {
|
||||
box-shadow: var(--shadow-md);
|
||||
transform: var(--hover-scale-sm);
|
||||
}
|
||||
|
||||
.theme-name-label {
|
||||
color: var(--text-color);
|
||||
display: block;
|
||||
font-size: var(--font-size-sm);
|
||||
font-weight: var(--font-weight-bold);
|
||||
margin-bottom: var(--margin-xs);
|
||||
}
|
||||
|
||||
.theme-name-section {
|
||||
box-sizing: border-box;
|
||||
margin-bottom: var(--margin-lg);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.themes-list {
|
||||
border: 1px solid var(--sidebar-border-color);
|
||||
border-radius: var(--border-radius-md);
|
||||
max-height: 300px;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.toggle-button {
|
||||
background-color: var(--secondary-bg);
|
||||
border: none;
|
||||
border-radius: var(--border-radius-md);
|
||||
color: var(--text-color);
|
||||
cursor: pointer;
|
||||
flex: 1;
|
||||
font-size: var(--font-size-base);
|
||||
font-weight: var(--font-weight-normal);
|
||||
padding: var(--padding-sm);
|
||||
transition: background-color var(--transition-fast), box-shadow var(--transition-fast), color var(--transition-fast), transform var(--transition-fast);
|
||||
}
|
||||
|
||||
.toggle-button.active {
|
||||
background-color: var(--main-fg);
|
||||
color: var(--text-color);
|
||||
font-weight: var(--font-weight-demi-bold);
|
||||
}
|
||||
|
||||
.toggle-button:not(.active):hover {
|
||||
background-color: var(--main-fg);
|
||||
box-shadow: var(--shadow-md);
|
||||
font-weight: var(--font-weight-demi-bold);
|
||||
transform: var(--hover-scale-sm);
|
||||
}
|
||||
|
||||
.turn-signal-help-text {
|
||||
background-color: var(--secondary-bg);
|
||||
border-radius: var(--border-radius-md);
|
||||
margin-top: 0.5em;
|
||||
padding: 0.5em;
|
||||
}
|
||||
|
||||
.turn-signal-help-text p {
|
||||
font-weight: normal;
|
||||
margin: 0.25em 0;
|
||||
}
|
||||
|
||||
.turn-signal-input {
|
||||
-moz-appearance: textfield;
|
||||
background-color: var(--secondary-bg);
|
||||
border: none;
|
||||
border-radius: var(--border-radius-md);
|
||||
box-sizing: border-box;
|
||||
color: var(--text-color);
|
||||
font-size: var(--font-size-base);
|
||||
padding: var(--padding-sm);
|
||||
transition: box-shadow var(--transition-fast), transform var(--transition-fast);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.turn-signal-input::-webkit-inner-spin-button,
|
||||
.turn-signal-input::-webkit-outer-spin-button {
|
||||
-webkit-appearance: none;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.turn-signal-label {
|
||||
font-size: var(--font-size-base);
|
||||
font-weight: var(--font-weight-demi-bold);
|
||||
}
|
||||
|
||||
.turn-signal-length-section,
|
||||
.turn-signal-style-section {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--gap-xs);
|
||||
}
|
||||
|
||||
#themeName,
|
||||
#submitThemeName {
|
||||
background-color: var(--input-bg);
|
||||
border: var(--border-style-input);
|
||||
border-radius: var(--border-radius-sm);
|
||||
box-sizing: border-box;
|
||||
color: var(--text-color);
|
||||
font-size: var(--font-size-sm);
|
||||
padding: var(--padding-sm);
|
||||
transition: border-color var(--transition-fast), box-shadow var(--transition-fast), transform var(--transition-fast);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
#themeName:focus,
|
||||
#themeName:hover,
|
||||
#submitThemeName:focus,
|
||||
#submitThemeName:hover,
|
||||
.discord-username-input:focus,
|
||||
.discord-username-input:hover,
|
||||
.turn-signal-input:focus,
|
||||
.turn-signal-input:hover {
|
||||
border-color: var(--thumb-color);
|
||||
box-shadow: 0 0 0 2px var(--thumb-color), 0 0 8px var(--thumb-color);
|
||||
outline: none;
|
||||
transform: var(--hover-scale-sm);
|
||||
}
|
||||
|
||||
.sequence-order-button {
|
||||
background-color: var(--main-fg);
|
||||
border: none;
|
||||
border-radius: var(--border-radius-md);
|
||||
color: var(--text-color);
|
||||
cursor: pointer;
|
||||
font-size: var(--font-size-base);
|
||||
font-weight: var(--font-weight-demi-bold);
|
||||
margin-top: var(--margin-sm);
|
||||
padding: var(--padding-sm) var(--padding-base);
|
||||
text-align: center;
|
||||
transition: background-color var(--transition-fast), box-shadow var(--transition-fast), transform var(--transition-fast);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.sequence-order-button:hover {
|
||||
background-color: var(--main-fg);
|
||||
box-shadow: var(--shadow-md);
|
||||
transform: var(--hover-scale-sm);
|
||||
}
|
||||
|
||||
.draggable-list {
|
||||
list-style-type: none;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.draggable-item {
|
||||
align-items: center;
|
||||
background-color: var(--input-bg);
|
||||
border: 1px solid var(--sidebar-border-color);
|
||||
border-radius: var(--border-radius-sm);
|
||||
cursor: move;
|
||||
display: flex;
|
||||
margin-bottom: 5px;
|
||||
padding: 10px;
|
||||
transition: background-color 0.2s, opacity 0.2s;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.draggable-item.dragging {
|
||||
opacity: 0.5;
|
||||
background-color: var(--main-fg);
|
||||
}
|
||||
|
||||
.draggable-item:hover {
|
||||
background-color: var(--main-fg);
|
||||
}
|
||||
|
||||
.draggable-item.drop-before::before,
|
||||
.draggable-item.drop-after::after {
|
||||
bottom: -2px;
|
||||
content: '';
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 2px;
|
||||
background-color: var(--success-bg);
|
||||
}
|
||||
|
||||
.draggable-item.drop-before::before {
|
||||
top: -2px;
|
||||
}
|
||||
|
||||
.sequential-image-preview {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
object-fit: contain;
|
||||
margin-right: 10px;
|
||||
vertical-align: middle;
|
||||
}
|
||||
|
||||
@keyframes modalFadeIn {
|
||||
from { opacity: 0; transform: scale(0.95); }
|
||||
to { opacity: 1; transform: scale(1); }
|
||||
}
|
||||
|
||||
@media only screen and (max-width: 768px) and (orientation: portrait) {
|
||||
.manage-themes-tabs {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.manage-themes-tabs .tab-button {
|
||||
border-bottom: 1px solid var(--sidebar-border-color);
|
||||
border-right: 1px solid var(--sidebar-border-color);
|
||||
flex: 1 0 33.333%;
|
||||
}
|
||||
|
||||
.manage-themes-tabs .tab-button:nth-child(3n) {
|
||||
border-right: none;
|
||||
}
|
||||
|
||||
.save-button-wrapper {
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.save-button-wrapper > button {
|
||||
flex-basis: calc(50% - 1rem);
|
||||
margin-bottom: var(--gap-sm);
|
||||
}
|
||||
}
|
||||
@@ -28,6 +28,7 @@
|
||||
<link rel="stylesheet" href="/assets/components/tools/doors.css">
|
||||
<link rel="stylesheet" href="/assets/components/tools/error_logs.css">
|
||||
<link rel="stylesheet" href="/assets/components/tools/speed_limits.css">
|
||||
<link rel="stylesheet" href="/assets/components/tools/theme_maker.css">
|
||||
<link rel="stylesheet" href="/assets/components/tools/tmux.css">
|
||||
<link rel="stylesheet" href="/assets/components/tools/toggles.css">
|
||||
<link rel="stylesheet" href="/assets/components/tools/tsk_manager.css">
|
||||
|
||||
@@ -6,6 +6,7 @@ from io import BytesIO
|
||||
from pathlib import Path
|
||||
from werkzeug.utils import secure_filename
|
||||
|
||||
import base64
|
||||
import errno
|
||||
import hashlib
|
||||
import json
|
||||
@@ -13,6 +14,7 @@ import os
|
||||
import re
|
||||
import requests
|
||||
import secrets
|
||||
import shutil
|
||||
import signal
|
||||
import subprocess
|
||||
import time
|
||||
@@ -28,11 +30,18 @@ from openpilot.system.loggerd.deleter import PRESERVE_ATTR_NAME, PRESERVE_ATTR_V
|
||||
from openpilot.system.version import get_build_metadata
|
||||
from panda import Panda
|
||||
|
||||
from openpilot.frogpilot.common.frogpilot_utilities import delete_file, get_lock_status, run_cmd
|
||||
from openpilot.frogpilot.common.frogpilot_variables import ERROR_LOGS_PATH, EXCLUDED_KEYS, SCREEN_RECORDINGS_PATH,\
|
||||
frogpilot_default_params, params, update_frogpilot_toggles
|
||||
from openpilot.frogpilot.assets.theme_manager import HOLIDAY_THEME_PATH, THEME_COMPONENT_PARAMS
|
||||
from openpilot.frogpilot.common.frogpilot_utilities import delete_file, get_lock_status, run_cmd, extract_tar
|
||||
from openpilot.frogpilot.common.frogpilot_variables import ACTIVE_THEME_PATH, ERROR_LOGS_PATH, EXCLUDED_KEYS, RESOURCES_REPO, SCREEN_RECORDINGS_PATH, THEME_SAVE_PATH,\
|
||||
frogpilot_default_params, params, params_memory, update_frogpilot_toggles
|
||||
from openpilot.frogpilot.system.the_pond import utilities
|
||||
|
||||
DISCORD_WEBHOOK_URL = os.getenv("DISCORD_WEBHOOK_URL")
|
||||
|
||||
GITLAB_API = "https://gitlab.com/api/v4"
|
||||
GITLAB_SUBMISSIONS_PROJECT_ID = "71992109"
|
||||
GITLAB_TOKEN = os.environ.get("GITLAB_TOKEN", "")
|
||||
|
||||
FOOTAGE_PATHS = [
|
||||
Paths.log_root(HD=True, raw=True),
|
||||
Paths.log_root(konik=True, raw=True),
|
||||
@@ -71,7 +80,8 @@ def setup(app):
|
||||
|
||||
while True:
|
||||
with Panda(disable_checks=True) as panda:
|
||||
panda.set_safety_mode(panda.SAFETY_TOYOTA)
|
||||
if not params.get_bool("IsOnroad"):
|
||||
panda.set_safety_mode(panda.SAFETY_TOYOTA)
|
||||
panda.can_send(0x750, LOCK_CMD, 0)
|
||||
|
||||
time.sleep(1)
|
||||
@@ -89,7 +99,8 @@ def setup(app):
|
||||
|
||||
while True:
|
||||
with Panda(disable_checks=True) as panda:
|
||||
panda.set_safety_mode(panda.SAFETY_TOYOTA)
|
||||
if not params.get_bool("IsOnroad"):
|
||||
panda.set_safety_mode(panda.SAFETY_TOYOTA)
|
||||
panda.can_send(0x750, UNLOCK_CMD, 0)
|
||||
|
||||
time.sleep(1)
|
||||
@@ -293,6 +304,10 @@ def setup(app):
|
||||
def get_param():
|
||||
return params.get(request.args.get("key")) or "", 200
|
||||
|
||||
@app.route("/api/params_memory", methods=["GET"])
|
||||
def get_param_memory():
|
||||
return params_memory.get(request.args.get("key")) or "", 200
|
||||
|
||||
@app.route("/api/routes", methods=["GET"])
|
||||
def list_routes():
|
||||
def generate():
|
||||
@@ -306,6 +321,27 @@ def setup(app):
|
||||
try:
|
||||
result = future.result()
|
||||
yield f"data: {json.dumps({'routes': [result]})}\n\n"
|
||||
|
||||
path, name = futures[future]
|
||||
segments = utilities.get_segments_in_route(name, path)
|
||||
if segments:
|
||||
for camera, cam_file in {
|
||||
"forward": "fcamera.hevc",
|
||||
"wide": "ecamera.hevc",
|
||||
"driver": "dcamera.hevc"
|
||||
}.items():
|
||||
input_files = [
|
||||
os.path.join(path, seg, cam_file)
|
||||
for seg in segments
|
||||
if os.path.exists(os.path.join(path, seg, cam_file))
|
||||
]
|
||||
if input_files:
|
||||
executor.submit(
|
||||
utilities.ffmpeg_concat_segments_to_mp4,
|
||||
input_files,
|
||||
f"{name}-{camera}"
|
||||
)
|
||||
|
||||
except Exception as exception:
|
||||
print(f"Error processing route: {exception}")
|
||||
yield f"data: {json.dumps({'progress': processed, 'total': total})}\n\n"
|
||||
@@ -370,6 +406,32 @@ def setup(app):
|
||||
return {"message": "Route unpreserved!"}, 200
|
||||
return {"error": "Route not found"}, 404
|
||||
|
||||
@app.route("/video/<name>/combined", methods=["GET"])
|
||||
def get_combined_route_video(name):
|
||||
camera = request.args.get("camera", "forward")
|
||||
for footage_path in FOOTAGE_PATHS:
|
||||
segments = utilities.get_segments_in_route(name, footage_path)
|
||||
if segments:
|
||||
cam_file = {
|
||||
"forward": "fcamera.hevc",
|
||||
"wide": "ecamera.hevc",
|
||||
"driver": "dcamera.hevc",
|
||||
}.get(camera, "fcamera.hevc")
|
||||
|
||||
input_files = [
|
||||
os.path.join(footage_path, seg, cam_file)
|
||||
for seg in segments
|
||||
if os.path.exists(os.path.join(footage_path, seg, cam_file))
|
||||
]
|
||||
|
||||
if not input_files:
|
||||
return {"error": "No video files found"}, 404
|
||||
|
||||
mp4_file = utilities.ffmpeg_concat_segments_to_mp4(input_files, cache_key=f"{name}-{camera}")
|
||||
return send_file(mp4_file, mimetype="video/mp4")
|
||||
|
||||
return {"error": "Route not found"}, 404
|
||||
|
||||
@app.route("/api/routes/<name>", methods=["GET"])
|
||||
def get_route(name):
|
||||
for footage_path in FOOTAGE_PATHS:
|
||||
@@ -652,7 +714,9 @@ def setup(app):
|
||||
os.makedirs(state, exist_ok=True)
|
||||
|
||||
run_cmd(["curl", "-fsSL", tgz_url, "-o", tgz_path], "Downloaded Tailscale archive.", "Failed to download Tailscale archive.")
|
||||
run_cmd(["tar", "xzf", tgz_path, "-C", base], "Extracted Tailscale archive.", "Failed to extract Tailscale archive.")
|
||||
|
||||
extract_tar(tgz_path, base)
|
||||
|
||||
run_cmd(["cp", f"{bin_dir}/tailscale", f"{base}/tailscale"], "Copied tailscale binary.", "Failed to copy tailscale binary.")
|
||||
run_cmd(["cp", f"{bin_dir}/tailscaled", f"{base}/tailscaled"], "Copied tailscaled binary.", "Failed to copy tailscaled binary.")
|
||||
run_cmd(["chmod", "+x", f"{base}/tailscale", f"{base}/tailscaled"], "Made binaries executable.", "Failed to chmod binaries.")
|
||||
@@ -740,6 +804,591 @@ def setup(app):
|
||||
|
||||
return jsonify({"message": "Tailscale uninstalled!"}), 200
|
||||
|
||||
@app.route("/api/themes", methods=["POST"])
|
||||
def save_theme_route():
|
||||
theme_path, error = utilities.create_theme(request.form, request.files)
|
||||
if error:
|
||||
return jsonify({"message": error}), 400
|
||||
return jsonify({"message": f'Theme "{request.form.get("themeName")}" saved!'}), 200
|
||||
|
||||
@app.route("/api/themes/download_asset", methods=["POST"])
|
||||
def start_download_asset():
|
||||
data = request.get_json() or {}
|
||||
raw_component = (data.get("component") or "").strip()
|
||||
display_name = (data.get("name") or "").strip()
|
||||
if not raw_component or not display_name:
|
||||
return jsonify({"error": "Missing component or name"}), 400
|
||||
|
||||
component = "steering_wheels" if raw_component == "steering_wheel" else ("signals" if raw_component == "turn_signals" else raw_component)
|
||||
mem_key = THEME_COMPONENT_PARAMS.get(component)
|
||||
if not mem_key:
|
||||
return jsonify({"error": "Unknown component"}), 400
|
||||
|
||||
slug = display_name.lower().replace("(", "").replace(")", "").replace(" ", "_")
|
||||
|
||||
params_memory.put(mem_key, slug)
|
||||
params_memory.put("ThemeDownloadProgress", "Downloading...")
|
||||
|
||||
return jsonify({"message": "Download started", "component": component, "param": mem_key, "slug": slug}), 200
|
||||
|
||||
@app.route("/api/themes/apply", methods=["POST"])
|
||||
def apply_theme():
|
||||
form_data = request.form.to_dict(flat=True)
|
||||
files = request.files
|
||||
|
||||
if not form_data.get("themeName"):
|
||||
form_data["themeName"] = f"tmp_{secrets.token_hex(8)}"
|
||||
|
||||
temp_path, error = utilities.create_theme(form_data, files, temporary=True)
|
||||
if error:
|
||||
return {"error": error}, 400
|
||||
|
||||
save_checklist = json.loads(form_data.get("saveChecklist", "{}"))
|
||||
|
||||
if save_checklist.get("colors"):
|
||||
asset_location = temp_path / "colors"
|
||||
save_location = ACTIVE_THEME_PATH / "colors"
|
||||
if save_location.exists() or save_location.is_symlink():
|
||||
delete_file(save_location)
|
||||
if asset_location.exists():
|
||||
save_location.parent.mkdir(parents=True, exist_ok=True)
|
||||
save_location.symlink_to(asset_location, target_is_directory=True)
|
||||
|
||||
if save_checklist.get("distance_icons"):
|
||||
asset_location = temp_path / "distance_icons"
|
||||
save_location = ACTIVE_THEME_PATH / "distance_icons"
|
||||
if save_location.exists() or save_location.is_symlink():
|
||||
delete_file(save_location)
|
||||
if asset_location.exists():
|
||||
save_location.parent.mkdir(parents=True, exist_ok=True)
|
||||
save_location.symlink_to(asset_location, target_is_directory=True)
|
||||
|
||||
if save_checklist.get("icons"):
|
||||
asset_location = temp_path / "icons"
|
||||
save_location = ACTIVE_THEME_PATH / "icons"
|
||||
if save_location.exists() or save_location.is_symlink():
|
||||
delete_file(save_location)
|
||||
if asset_location.exists():
|
||||
save_location.parent.mkdir(parents=True, exist_ok=True)
|
||||
save_location.symlink_to(asset_location, target_is_directory=True)
|
||||
|
||||
if save_checklist.get("sounds"):
|
||||
asset_location = temp_path / "sounds"
|
||||
save_location = ACTIVE_THEME_PATH / "sounds"
|
||||
if save_location.exists() or save_location.is_symlink():
|
||||
delete_file(save_location)
|
||||
if asset_location.exists():
|
||||
save_location.parent.mkdir(parents=True, exist_ok=True)
|
||||
save_location.symlink_to(asset_location, target_is_directory=True)
|
||||
|
||||
if save_checklist.get("turn_signals"):
|
||||
asset_location = temp_path / "signals"
|
||||
save_location = ACTIVE_THEME_PATH / "signals"
|
||||
if save_location.exists() or save_location.is_symlink():
|
||||
delete_file(save_location)
|
||||
if asset_location.exists():
|
||||
save_location.parent.mkdir(parents=True, exist_ok=True)
|
||||
save_location.symlink_to(asset_location, target_is_directory=True)
|
||||
|
||||
wheel_location = temp_path / "WheelIcon"
|
||||
wheel_save_location = ACTIVE_THEME_PATH / "steering_wheel"
|
||||
if wheel_location.exists():
|
||||
if wheel_save_location.exists():
|
||||
delete_file(wheel_save_location)
|
||||
|
||||
wheel_save_location.mkdir(parents=True, exist_ok=True)
|
||||
for file in wheel_location.iterdir():
|
||||
destination_file = wheel_save_location / file.name
|
||||
delete_file(destination_file)
|
||||
destination_file.symlink_to(file)
|
||||
|
||||
params.put_bool("PersonalizeOpenpilot", True)
|
||||
params_memory.put_bool("UseActiveTheme", True)
|
||||
|
||||
update_frogpilot_toggles()
|
||||
return {"message": "Theme applied successfully!"}, 200
|
||||
|
||||
@app.route("/api/themes/asset/<path:theme>/<path:asset_path>")
|
||||
def get_theme_asset(theme, asset_path):
|
||||
theme_type = request.args.get("type", "")
|
||||
|
||||
if theme_type == "active" or theme == "__active__":
|
||||
file_path = ACTIVE_THEME_PATH / asset_path
|
||||
elif asset_path.startswith("steering_wheels/"):
|
||||
file_path = THEME_SAVE_PATH / asset_path
|
||||
elif asset_path.startswith("steering_wheel/") and "holiday" in theme_type:
|
||||
file_path = HOLIDAY_THEME_PATH / theme / asset_path
|
||||
else:
|
||||
base_dir = HOLIDAY_THEME_PATH / theme if "holiday" in theme_type else THEME_SAVE_PATH / "theme_packs" / theme
|
||||
file_path = base_dir / asset_path
|
||||
|
||||
if not file_path.exists():
|
||||
return "File not found", 404
|
||||
|
||||
return send_file(file_path, as_attachment=False)
|
||||
|
||||
@app.route("/api/themes/delete/<path:theme_path_str>", methods=["DELETE"])
|
||||
def delete_theme(theme_path_str):
|
||||
theme_type = request.args.get("type", "user")
|
||||
component = (request.args.get("component") or "").strip()
|
||||
|
||||
if theme_type == "holiday":
|
||||
return jsonify({"message": "Cannot delete holiday themes."}), 403
|
||||
|
||||
if theme_type == "steering_wheel":
|
||||
wheel_path = THEME_SAVE_PATH / "steering_wheels" / theme_path_str
|
||||
if wheel_path.exists():
|
||||
delete_file(wheel_path)
|
||||
return jsonify({"message": f'Steering wheel "{utilities.normalize_theme_name(wheel_path.stem)}" deleted!'}), 200
|
||||
return jsonify({"message": "Steering wheel not found..."}), 404
|
||||
|
||||
theme_path = THEME_SAVE_PATH / "theme_packs" / theme_path_str
|
||||
if not theme_path.is_dir():
|
||||
return jsonify({"message": "Theme not found..."}), 404
|
||||
|
||||
if component:
|
||||
allowed = {"colors", "distance_icons", "icons", "sounds", "signals"}
|
||||
if component not in allowed:
|
||||
return jsonify({"message": "Unknown component..."}), 400
|
||||
|
||||
target = theme_path / component
|
||||
if not target.exists():
|
||||
return jsonify({"message": f'Component "{component}" not found in theme...'}), 404
|
||||
|
||||
delete_file(target)
|
||||
|
||||
return jsonify({"message": f'Removed {component.replace("_", " ")} from "{utilities.normalize_theme_name(theme_path.name)}"!'}), 200
|
||||
|
||||
delete_file(theme_path)
|
||||
return jsonify({"message": f'Theme "{utilities.normalize_theme_name(theme_path.name)}" deleted!'}), 200
|
||||
|
||||
@app.route("/api/themes/default", methods=["GET"])
|
||||
def get_default_theme():
|
||||
theme_data = {
|
||||
"colors": {},
|
||||
"images": {},
|
||||
"sounds": {},
|
||||
"turnSignalLength": 100,
|
||||
"turnSignalType": "Single Image",
|
||||
"sequentialImages": [],
|
||||
"theme_names": {}
|
||||
}
|
||||
|
||||
if not params.get_bool("PersonalizeOpenpilot"):
|
||||
theme_data["theme_names"] = {
|
||||
"colors": "Stock",
|
||||
"distanceIcons": "Stock",
|
||||
"icons": "Stock",
|
||||
"sounds": "Stock",
|
||||
"turnSignals": "Stock",
|
||||
"steeringWheel": "Stock"
|
||||
}
|
||||
else:
|
||||
theme_param_map = {
|
||||
"CustomColors": "colors",
|
||||
"CustomDistanceIcons": "distanceIcons",
|
||||
"CustomIcons": "icons",
|
||||
"CustomSounds": "sounds",
|
||||
"CustomSignals": "turnSignals",
|
||||
"WheelIcon": "steeringWheel"
|
||||
}
|
||||
for param, theme_key in theme_param_map.items():
|
||||
param_value = params.get(param, encoding="utf-8")
|
||||
if param_value:
|
||||
theme_data["theme_names"][theme_key] = utilities.normalize_theme_name(param_value)
|
||||
|
||||
colors_path = ACTIVE_THEME_PATH / "colors" / "colors.json"
|
||||
if colors_path.exists():
|
||||
with open(colors_path, "r") as f:
|
||||
theme_data["colors"] = json.load(f)
|
||||
|
||||
signals_dir = ACTIVE_THEME_PATH / "signals"
|
||||
if signals_dir.exists():
|
||||
sequential_files = sorted([f.name for f in signals_dir.glob("turn_signal_*.png") if "blindspot" not in f.name.lower()])
|
||||
if sequential_files:
|
||||
theme_data["sequentialImages"] = sequential_files
|
||||
theme_data["turnSignalType"] = "Sequential"
|
||||
|
||||
theme_data["turnSignalStyle"] = "Traditional"
|
||||
theme_data["turnSignalLength"] = 100
|
||||
|
||||
for file in os.listdir(signals_dir):
|
||||
if not any(file.endswith(ext) for ext in [".png", ".gif", ".jpg", ".jpeg"]):
|
||||
parts = file.split("_")
|
||||
if len(parts) == 2:
|
||||
theme_data["turnSignalStyle"] = parts[0].capitalize()
|
||||
try:
|
||||
theme_data["turnSignalLength"] = int(parts[1])
|
||||
except ValueError:
|
||||
pass
|
||||
break
|
||||
|
||||
exts = [".png", ".gif", ".jpg", ".jpeg"]
|
||||
for ext in exts:
|
||||
p = signals_dir / f"turn_signal{ext}"
|
||||
if p.exists():
|
||||
theme_data["images"]["turnSignal"] = f"turn_signal{ext}"
|
||||
break
|
||||
for ext in exts:
|
||||
p = signals_dir / f"turn_signal_blindspot{ext}"
|
||||
if p.exists():
|
||||
theme_data["images"]["turnSignalBlindspot"] = f"turn_signal_blindspot{ext}"
|
||||
break
|
||||
|
||||
icons_path = ACTIVE_THEME_PATH / "icons"
|
||||
if icons_path.exists() and icons_path.is_dir():
|
||||
for file in os.listdir(icons_path):
|
||||
if Path(file).stem == "button_settings":
|
||||
theme_data["images"]["settingsButton"] = file
|
||||
elif Path(file).stem == "button_home":
|
||||
theme_data["images"]["homeButton"] = file
|
||||
|
||||
wheel_path = ACTIVE_THEME_PATH / "steering_wheel"
|
||||
if wheel_path.exists() and wheel_path.is_dir():
|
||||
wheel_files = list(wheel_path.glob("wheel.*"))
|
||||
if wheel_files:
|
||||
theme_data["images"]["steeringWheel"] = wheel_files[0].name
|
||||
|
||||
distance_icons_path = ACTIVE_THEME_PATH / "distance_icons"
|
||||
if distance_icons_path.exists() and distance_icons_path.is_dir():
|
||||
theme_data["images"]["distanceIcons"] = {}
|
||||
for file in os.listdir(distance_icons_path):
|
||||
key = Path(file).stem
|
||||
if key in ["traffic", "aggressive", "standard", "relaxed"]:
|
||||
theme_data["images"]["distanceIcons"][key] = file
|
||||
|
||||
sounds_path = ACTIVE_THEME_PATH / "sounds"
|
||||
if sounds_path.exists() and sounds_path.is_dir():
|
||||
valid_sound_keys = ["engage", "disengage", "prompt", "startup"]
|
||||
for file in os.listdir(sounds_path):
|
||||
stem = Path(file).stem
|
||||
if stem in valid_sound_keys:
|
||||
theme_data["sounds"][stem] = file
|
||||
|
||||
return jsonify(theme_data)
|
||||
|
||||
@app.route("/api/themes/download", methods=["POST"])
|
||||
def download_theme_route():
|
||||
theme_path, error = utilities.create_theme(request.form, request.files, temporary=True)
|
||||
if error:
|
||||
return jsonify({"message": error}), 400
|
||||
|
||||
sane_theme_name = utilities.normalize_theme_name(request.form.get("themeName"), for_path=True)
|
||||
|
||||
archive_path = shutil.make_archive(str(theme_path.parent / sane_theme_name), "zip", theme_path.parent, sane_theme_name)
|
||||
|
||||
memory_file = BytesIO()
|
||||
with open(archive_path, "rb") as f:
|
||||
memory_file.write(f.read())
|
||||
memory_file.seek(0)
|
||||
|
||||
delete_file(theme_path.parent)
|
||||
|
||||
return send_file(memory_file, download_name=f'{sane_theme_name}.zip', as_attachment=True)
|
||||
|
||||
@app.route("/api/themes/list", methods=["GET"])
|
||||
def list_themes():
|
||||
all_themes = []
|
||||
themes_path = THEME_SAVE_PATH / "theme_packs"
|
||||
|
||||
if themes_path.exists():
|
||||
for theme_dir in themes_path.iterdir():
|
||||
if theme_dir.is_dir():
|
||||
is_user_created = "-user_created" in theme_dir.name
|
||||
components = utilities.check_theme_components(theme_dir)
|
||||
all_themes.append({
|
||||
"name": utilities.normalize_theme_name(theme_dir.name),
|
||||
"path": theme_dir.name,
|
||||
"type": "user" if is_user_created else "standard",
|
||||
"is_user_created": is_user_created,
|
||||
**components
|
||||
})
|
||||
|
||||
if HOLIDAY_THEME_PATH.exists():
|
||||
for theme_dir in HOLIDAY_THEME_PATH.iterdir():
|
||||
if theme_dir.is_dir():
|
||||
components = utilities.check_theme_components(theme_dir)
|
||||
all_themes.append({
|
||||
"name": utilities.normalize_theme_name(theme_dir.name),
|
||||
"path": theme_dir.name,
|
||||
"type": "holiday",
|
||||
"is_user_created": False,
|
||||
**components
|
||||
})
|
||||
|
||||
wheels_path = THEME_SAVE_PATH / "steering_wheels"
|
||||
if wheels_path.exists():
|
||||
for wheel_file in wheels_path.iterdir():
|
||||
all_themes.append({
|
||||
"name": utilities.normalize_theme_name(wheel_file.stem),
|
||||
"path": wheel_file.name,
|
||||
"type": "steering_wheel",
|
||||
"is_user_created": "-user_created" in wheel_file.name,
|
||||
"hasSteeringWheel": True,
|
||||
})
|
||||
|
||||
return jsonify({"themes": sorted(all_themes, key=lambda x: x['name'])})
|
||||
|
||||
@app.route("/api/themes/load/<path:theme_path>")
|
||||
def load_theme(theme_path):
|
||||
theme_type = request.args.get("type", "")
|
||||
theme_dir = HOLIDAY_THEME_PATH / theme_path if "holiday" in theme_type else THEME_SAVE_PATH / "theme_packs" / theme_path
|
||||
|
||||
response_data = {
|
||||
"colors": None,
|
||||
"images": {},
|
||||
"sounds": {},
|
||||
"sequentialImages": [],
|
||||
"turnSignalType": "Single Image",
|
||||
"turnSignalStyle": "Static",
|
||||
"turnSignalLength": 100
|
||||
}
|
||||
|
||||
colors_file = theme_dir / "colors" / "colors.json"
|
||||
if colors_file.exists():
|
||||
with open(colors_file) as f:
|
||||
response_data["colors"] = json.load(f)
|
||||
|
||||
icons_dir = theme_dir / "icons"
|
||||
if icons_dir.exists():
|
||||
if (icons_dir / "button_home.gif").exists():
|
||||
response_data["images"]["homeButton"] = {
|
||||
"filename": "button_home.gif",
|
||||
"path": "icons/button_home.gif"
|
||||
}
|
||||
if (icons_dir / "button_settings.png").exists():
|
||||
response_data["images"]["settingsButton"] = {
|
||||
"filename": "button_settings.png",
|
||||
"path": "icons/button_settings.png"
|
||||
}
|
||||
|
||||
distance_dir = theme_dir / "distance_icons"
|
||||
if distance_dir.exists():
|
||||
response_data["images"]["distanceIcons"] = {}
|
||||
exts = [".png", ".gif", ".jpg", ".jpeg"]
|
||||
for name in ["aggressive", "relaxed", "standard", "traffic"]:
|
||||
for ext in exts:
|
||||
p = distance_dir / f"{name}{ext}"
|
||||
if p.exists():
|
||||
response_data["images"]["distanceIcons"][name] = {
|
||||
"filename": f"{name}{ext}",
|
||||
"path": f"distance_icons/{name}{ext}"
|
||||
}
|
||||
break
|
||||
|
||||
signals_dir = theme_dir / "signals"
|
||||
if signals_dir.exists():
|
||||
sequential_files = sorted([f.name for f in signals_dir.glob("turn_signal_*.png") if "blindspot" not in f.name.lower()])
|
||||
if sequential_files:
|
||||
response_data["sequentialImages"] = sequential_files
|
||||
response_data["turnSignalType"] = "Sequential"
|
||||
|
||||
response_data["turnSignalStyle"] = "Traditional"
|
||||
response_data["turnSignalLength"] = 100
|
||||
|
||||
for file in os.listdir(signals_dir):
|
||||
if not any(file.endswith(ext) for ext in [".png", ".gif", ".jpg", ".jpeg"]):
|
||||
parts = file.split("_")
|
||||
if len(parts) == 2:
|
||||
response_data["turnSignalStyle"] = parts[0].capitalize()
|
||||
try:
|
||||
response_data["turnSignalLength"] = int(parts[1])
|
||||
except ValueError:
|
||||
pass
|
||||
break
|
||||
|
||||
exts = [".png", ".gif", ".jpg", ".jpeg"]
|
||||
for ext in exts:
|
||||
p = signals_dir / f"turn_signal{ext}"
|
||||
if p.exists():
|
||||
response_data["images"]["turnSignal"] = {
|
||||
"filename": f"turn_signal{ext}",
|
||||
"path": f"signals/turn_signal{ext}",
|
||||
}
|
||||
break
|
||||
for ext in exts:
|
||||
p = signals_dir / f"turn_signal_blindspot{ext}"
|
||||
if p.exists():
|
||||
response_data["images"]["turnSignalBlindspot"] = {
|
||||
"filename": f"turn_signal_blindspot{ext}",
|
||||
"path": f"signals/turn_signal_blindspot{ext}",
|
||||
}
|
||||
break
|
||||
|
||||
sounds_dir = theme_dir / "sounds"
|
||||
if sounds_dir.exists():
|
||||
for name in ["engage", "disengage", "startup", "prompt"]:
|
||||
file_path = sounds_dir / f"{name}.wav"
|
||||
if file_path.exists():
|
||||
response_data["sounds"][name] = {
|
||||
"filename": f"{name}.wav",
|
||||
"path": f"sounds/{name}.wav"
|
||||
}
|
||||
|
||||
steering_wheel_path = None
|
||||
if "holiday" in theme_type:
|
||||
steering_dir = theme_dir / "steering_wheel"
|
||||
if steering_dir.exists() and steering_dir.is_dir():
|
||||
for file in steering_dir.iterdir():
|
||||
if file.is_file() and file.suffix.lower() in [".png", ".jpg", ".jpeg", ".gif"]:
|
||||
steering_wheel_path = f"steering_wheel/{file.name}"
|
||||
break
|
||||
else:
|
||||
steering_wheels_dir = THEME_SAVE_PATH / "steering_wheels"
|
||||
if steering_wheels_dir.exists():
|
||||
for file in steering_wheels_dir.iterdir():
|
||||
if file.is_file() and file.stem.lower() == theme_path.lower() and file.suffix.lower() in [".png", ".jpg", ".jpeg", ".gif"]:
|
||||
steering_wheel_path = f"steering_wheels/{file.name}"
|
||||
break
|
||||
|
||||
if steering_wheel_path:
|
||||
response_data["images"]["steeringWheel"] = {
|
||||
"filename": steering_wheel_path.split("/")[-1],
|
||||
"path": steering_wheel_path
|
||||
}
|
||||
|
||||
return jsonify(response_data)
|
||||
|
||||
@app.route("/api/themes/submit", methods=["POST"])
|
||||
def submit_theme():
|
||||
if not GITLAB_TOKEN:
|
||||
return jsonify({"error": "Missing GitLab token"}), 500
|
||||
|
||||
try:
|
||||
theme_name = request.form.get("themeName")
|
||||
if not theme_name:
|
||||
return jsonify({"error": "Missing theme name"}), 400
|
||||
|
||||
discord_username = request.form.get("discordUsername") or "Unknown"
|
||||
|
||||
theme_path, error = utilities.create_theme(request.form, request.files, temporary=True)
|
||||
if error:
|
||||
return jsonify({"message": error}), 400
|
||||
|
||||
safe_theme_name = utilities.normalize_theme_name(theme_name, for_path=True)
|
||||
combined_name = f"{safe_theme_name}~{discord_username}"
|
||||
timestamp = int(time.time())
|
||||
|
||||
def gitlab_post(project_id, endpoint, payload):
|
||||
url = f"{GITLAB_API}/projects/{project_id}/{endpoint}"
|
||||
resp = requests.post(url, headers={"PRIVATE-TOKEN": GITLAB_TOKEN}, json=payload)
|
||||
if resp.status_code not in (200, 201):
|
||||
raise RuntimeError(f"GitLab API error {resp.status_code}: {resp.text}")
|
||||
return resp.json()
|
||||
|
||||
def encode_file_base64(path):
|
||||
with open(path, "rb") as f:
|
||||
return base64.b64encode(f.read()).decode("utf-8")
|
||||
|
||||
def send_discord_notification(username, theme_name, asset_types):
|
||||
if not DISCORD_WEBHOOK_URL:
|
||||
return
|
||||
|
||||
message = (
|
||||
f"🎨 **New Theme Submission**\n"
|
||||
f"User: `{username}`\n"
|
||||
f"Theme: `{theme_name}`\n"
|
||||
f"Assets: {', '.join(asset_types)}\n"
|
||||
f"[View Submissions Repo](https://gitlab.com/{RESOURCES_REPO}-Submissions)\n"
|
||||
f"<@263565721336807424>"
|
||||
)
|
||||
payload = {"content": message}
|
||||
try:
|
||||
resp = requests.post(DISCORD_WEBHOOK_URL, json=payload)
|
||||
if resp.status_code not in (200, 204):
|
||||
print(f"Discord notification failed: {resp.status_code} {resp.text}")
|
||||
except Exception as exception:
|
||||
print(f"Error sending Discord message: {exception}")
|
||||
|
||||
asset_types = []
|
||||
submission_urls = {}
|
||||
|
||||
distance_icons_path = theme_path / "distance_icons"
|
||||
if distance_icons_path.exists() and any(distance_icons_path.iterdir()):
|
||||
zip_path = shutil.make_archive(str(distance_icons_path), "zip", distance_icons_path)
|
||||
encoded = encode_file_base64(zip_path)
|
||||
file_name = f"{combined_name}.zip"
|
||||
actions = [
|
||||
{
|
||||
"action": "create",
|
||||
"file_path": file_name,
|
||||
"content": encoded,
|
||||
"encoding": "base64"
|
||||
}
|
||||
]
|
||||
commit_payload = {
|
||||
"branch": "Distance-Icons",
|
||||
"commit_message": f"Added Distance Icons: {combined_name}",
|
||||
"actions": actions
|
||||
}
|
||||
gitlab_post(GITLAB_SUBMISSIONS_PROJECT_ID, "repository/commits", commit_payload)
|
||||
asset_types.append("Distance Icons")
|
||||
submission_urls["distance_icons"] = f"https://gitlab.com/{RESOURCES_REPO}-Submissions/-/tree/Distance-Icons"
|
||||
|
||||
theme_actions = []
|
||||
for folder in ["colors", "icons", "signals", "sounds"]:
|
||||
folder_path = theme_path / folder
|
||||
if folder_path.exists() and any(folder_path.iterdir()):
|
||||
zip_path = shutil.make_archive(str(folder_path), "zip", folder_path)
|
||||
encoded = encode_file_base64(zip_path)
|
||||
file_path = f"{combined_name}/{folder}.zip"
|
||||
theme_actions.append({
|
||||
"action": "create",
|
||||
"file_path": file_path,
|
||||
"content": encoded,
|
||||
"encoding": "base64"
|
||||
})
|
||||
|
||||
if theme_actions:
|
||||
commit_payload = {
|
||||
"branch": "Themes",
|
||||
"commit_message": f"Added Theme: {combined_name}",
|
||||
"actions": theme_actions
|
||||
}
|
||||
gitlab_post(GITLAB_SUBMISSIONS_PROJECT_ID, "repository/commits", commit_payload)
|
||||
asset_types.append("Theme")
|
||||
submission_urls["theme"] = f"https://gitlab.com/{RESOURCES_REPO}-Submissions/-/tree/Themes"
|
||||
|
||||
wheel_file = request.files.get("steeringWheel")
|
||||
if wheel_file and wheel_file.filename:
|
||||
suffix = Path(wheel_file.filename).suffix
|
||||
file_name = f"{combined_name}{suffix}"
|
||||
wheel_file.seek(0)
|
||||
encoded_wheel = base64.b64encode(wheel_file.read()).decode("utf-8")
|
||||
actions = [
|
||||
{
|
||||
"action": "create",
|
||||
"file_path": file_name,
|
||||
"content": encoded_wheel,
|
||||
"encoding": "base64"
|
||||
}
|
||||
]
|
||||
commit_payload = {
|
||||
"branch": "Steering-Wheels",
|
||||
"commit_message": f"Added Steering Wheel: {combined_name}",
|
||||
"actions": actions
|
||||
}
|
||||
gitlab_post(GITLAB_SUBMISSIONS_PROJECT_ID, "repository/commits", commit_payload)
|
||||
asset_types.append("Steering Wheel")
|
||||
submission_urls["steering_wheel"] = f"https://gitlab.com/{RESOURCES_REPO}-Submissions/-/tree/Steering-Wheels"
|
||||
|
||||
if not submission_urls:
|
||||
return jsonify({"error": "No valid theme data or steering wheel file provided"}), 400
|
||||
|
||||
send_discord_notification(discord_username, theme_name, asset_types)
|
||||
|
||||
return jsonify({
|
||||
"message": "Submission successful!",
|
||||
"branches": submission_urls
|
||||
}), 200
|
||||
|
||||
except Exception as exception:
|
||||
return jsonify({"error": str(exception)}), 500
|
||||
|
||||
finally:
|
||||
if "theme_path" in locals() and theme_path.parent.exists():
|
||||
delete_file(theme_path.parent)
|
||||
|
||||
@app.route("/api/tmux_log/capture", methods=["POST"])
|
||||
def capture_tmux_log_route():
|
||||
TMUX_LOGS_PATH.mkdir(parents=True, exist_ok=True)
|
||||
@@ -924,16 +1573,65 @@ def setup(app):
|
||||
def get_video(path):
|
||||
camera = request.args.get("camera")
|
||||
filename = {"driver": "dcamera.hevc", "wide": "ecamera.hevc"}.get(camera, "fcamera.hevc")
|
||||
|
||||
for footage_path in FOOTAGE_PATHS:
|
||||
filepath = f"{footage_path}{path}/{filename}"
|
||||
if os.path.exists(filepath):
|
||||
process = utilities.ffmpeg_mp4_wrap_process_builder(filepath)
|
||||
return Response(process.stdout.read(), status=200, mimetype="video/mp4")
|
||||
file_handle = utilities.ffmpeg_mp4_wrap_process_builder(filepath)
|
||||
|
||||
file_handle.seek(0, 2)
|
||||
file_size = file_handle.tell()
|
||||
file_handle.seek(0)
|
||||
|
||||
range_header = request.headers.get('Range', None)
|
||||
if range_header:
|
||||
byte_start = 0
|
||||
byte_end = file_size - 1
|
||||
|
||||
if range_header.startswith('bytes='):
|
||||
range_spec = range_header[6:]
|
||||
if '-' in range_spec:
|
||||
start, end = range_spec.split('-', 1)
|
||||
if start:
|
||||
byte_start = max(0, int(start))
|
||||
if end:
|
||||
byte_end = min(file_size - 1, int(end))
|
||||
|
||||
if byte_start >= file_size:
|
||||
file_handle.close()
|
||||
return Response("Requested Range Not Satisfiable", 416)
|
||||
|
||||
byte_end = max(byte_start, byte_end)
|
||||
|
||||
file_handle.seek(byte_start)
|
||||
read_length = byte_end - byte_start + 1
|
||||
data = file_handle.read(read_length)
|
||||
|
||||
response = Response(
|
||||
data,
|
||||
206,
|
||||
headers={
|
||||
'Content-Range': f'bytes {byte_start}-{byte_end}/{file_size}',
|
||||
'Accept-Ranges': 'bytes',
|
||||
'Content-Length': str(len(data)),
|
||||
'Content-Type': 'video/mp4'
|
||||
}
|
||||
)
|
||||
else:
|
||||
data = file_handle.read()
|
||||
response = Response(
|
||||
data,
|
||||
200,
|
||||
headers={
|
||||
'Accept-Ranges': 'bytes',
|
||||
'Content-Length': str(file_size),
|
||||
'Content-Type': 'video/mp4'
|
||||
}
|
||||
)
|
||||
|
||||
file_handle.close()
|
||||
return response
|
||||
return {"error": "Video not found"}, 404
|
||||
|
||||
|
||||
def main():
|
||||
app = Flask(__name__, static_folder="assets", static_url_path="/assets")
|
||||
setup(app)
|
||||
|
||||
@@ -1,15 +1,21 @@
|
||||
#!/usr/bin/env python3
|
||||
import base64
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import secrets
|
||||
import shutil
|
||||
import subprocess
|
||||
import time
|
||||
import uuid
|
||||
|
||||
from datetime import datetime
|
||||
from pathlib import Path
|
||||
from PIL import Image
|
||||
from pydub import AudioSegment
|
||||
from typing import List
|
||||
from werkzeug.utils import secure_filename
|
||||
|
||||
from openpilot.common.conversions import Conversions as CV
|
||||
from openpilot.system.loggerd.config import get_available_bytes, get_used_bytes
|
||||
@@ -17,7 +23,8 @@ from openpilot.system.loggerd.deleter import PRESERVE_ATTR_NAME, PRESERVE_ATTR_V
|
||||
from openpilot.system.loggerd.uploader import listdir_by_creation
|
||||
from openpilot.tools.lib.route import SegmentName
|
||||
|
||||
from openpilot.frogpilot.common.frogpilot_variables import params, params_tracking
|
||||
from openpilot.frogpilot.common.frogpilot_variables import THEME_SAVE_PATH, VIDEO_CACHE_PATH, params
|
||||
from openpilot.frogpilot.assets.theme_manager import HOLIDAY_THEME_PATH
|
||||
|
||||
LOG_CANDIDATES = [
|
||||
"qlog",
|
||||
@@ -29,8 +36,293 @@ LOG_CANDIDATES = [
|
||||
|
||||
SEGMENT_RE = re.compile(r"^[0-9a-fA-F]{8}--[0-9a-fA-F]{10}--\d+$")
|
||||
|
||||
TARGET_LOUDNESS = -15.0
|
||||
|
||||
XOR_KEY = "s8#pL3*Xj!aZ@dWq"
|
||||
|
||||
MAX_FILE_SIZE = 5 * 1024 * 1024
|
||||
|
||||
def check_theme_components(theme_path):
|
||||
components = {
|
||||
"hasColors": False,
|
||||
"hasIcons": False,
|
||||
"hasSounds": False,
|
||||
"hasTurnSignals": False,
|
||||
"hasDistanceIcons": False,
|
||||
"hasSteeringWheel": False
|
||||
}
|
||||
|
||||
colors_path = theme_path / "colors" / "colors.json"
|
||||
if colors_path.exists():
|
||||
components["hasColors"] = True
|
||||
|
||||
icons_path = theme_path / "icons"
|
||||
if icons_path.exists() and any(icons_path.iterdir()):
|
||||
components["hasIcons"] = True
|
||||
|
||||
sounds_path = theme_path / "sounds"
|
||||
if sounds_path.exists() and any(sounds_path.iterdir()):
|
||||
components["hasSounds"] = True
|
||||
|
||||
signals_path = theme_path / "signals"
|
||||
if signals_path.exists() and any(signals_path.iterdir()):
|
||||
components["hasTurnSignals"] = True
|
||||
|
||||
distance_icons_path = theme_path / "distance_icons"
|
||||
if distance_icons_path.exists() and any(distance_icons_path.iterdir()):
|
||||
components["hasDistanceIcons"] = True
|
||||
|
||||
is_holiday_theme = str(HOLIDAY_THEME_PATH) in str(theme_path)
|
||||
|
||||
if is_holiday_theme:
|
||||
wheel_path = theme_path / "steering_wheel"
|
||||
if wheel_path.exists() and any(f.name.startswith("wheel.") for f in wheel_path.iterdir()):
|
||||
components["hasSteeringWheel"] = True
|
||||
else:
|
||||
wheel_path = THEME_SAVE_PATH / "steering_wheels"
|
||||
if wheel_path.exists():
|
||||
theme_name = theme_path.name.replace('-user_created', '')
|
||||
if any(wheel_path.glob(f"{theme_name}-user_created.*")):
|
||||
components["hasSteeringWheel"] = True
|
||||
|
||||
return components
|
||||
|
||||
def covert_audio(input_file):
|
||||
sound = AudioSegment.from_file(input_file)
|
||||
sound = sound.set_frame_rate(48000)
|
||||
sound = sound.set_channels(1)
|
||||
|
||||
output_filename = os.path.splitext(input_file)[0] + ".wav"
|
||||
sound.export(output_filename, format="wav", parameters=["-acodec", "pcm_s16le"])
|
||||
|
||||
if input_file != output_filename:
|
||||
os.remove(input_file)
|
||||
|
||||
def create_theme(form_data, files, temporary=False):
|
||||
theme_name = form_data.get("themeName")
|
||||
if not theme_name:
|
||||
return None, "Theme name is required."
|
||||
|
||||
sane_theme_name = secure_filename(theme_name.replace(" ", "_"))
|
||||
|
||||
save_checklist_str = form_data.get("saveChecklist", "{}")
|
||||
save_checklist = json.loads(save_checklist_str)
|
||||
|
||||
needs_theme_pack = any([
|
||||
save_checklist.get("colors"),
|
||||
save_checklist.get("icons"),
|
||||
save_checklist.get("sounds"),
|
||||
save_checklist.get("turn_signals"),
|
||||
save_checklist.get("distance_icons"),
|
||||
])
|
||||
|
||||
if temporary:
|
||||
base_path = Path(f"/tmp/{sane_theme_name}_{secrets.token_hex(8)}")
|
||||
else:
|
||||
base_path = THEME_SAVE_PATH / "theme_packs" if needs_theme_pack else None
|
||||
|
||||
theme_path = (base_path / f"{sane_theme_name}-user_created") if base_path else None
|
||||
if theme_path:
|
||||
theme_path.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
if save_checklist.get("colors"):
|
||||
(theme_path / "colors").mkdir(exist_ok=True)
|
||||
colors_str = form_data.get("colors")
|
||||
if colors_str:
|
||||
color_data = json.loads(colors_str)
|
||||
for key, values in color_data.items():
|
||||
if "alpha" in values:
|
||||
values["alpha"] = values.pop("alpha")
|
||||
colors_file = theme_path / "colors" / "colors.json"
|
||||
with open(colors_file, "w") as f:
|
||||
json.dump(color_data, f, indent=2)
|
||||
|
||||
if save_checklist.get("turn_signals"):
|
||||
signals_path = theme_path / "signals"
|
||||
signals_path.mkdir(exist_ok=True)
|
||||
|
||||
if turn_signal_length := form_data.get("turnSignalLength"):
|
||||
style = form_data.get("turnSignalStyle", "Traditional").lower()
|
||||
(signals_path / f"{style}_{turn_signal_length}").touch()
|
||||
|
||||
turn_signal_type = form_data.get("turnSignalType", "Single Image").lower()
|
||||
|
||||
if turn_signal_type == "single image":
|
||||
for f in signals_path.glob("turn_signal.*"):
|
||||
f.unlink()
|
||||
for f in signals_path.glob("turn_signal_blindspot.*"):
|
||||
f.unlink()
|
||||
|
||||
file = files.get("turnSignal")
|
||||
if file and file.filename:
|
||||
if file.content_length > MAX_FILE_SIZE:
|
||||
return None, f"File {file.filename} exceeds 1MB limit."
|
||||
ext = Path(file.filename).suffix
|
||||
file.save(signals_path / f"turn_signal{ext}")
|
||||
|
||||
file = files.get("turnSignalBlindspot")
|
||||
if file and file.filename:
|
||||
if file.content_length > MAX_FILE_SIZE:
|
||||
return None, f"File {file.filename} exceeds 1MB limit."
|
||||
ext = Path(file.filename).suffix
|
||||
file.save(signals_path / f"turn_signal_blindspot{ext}")
|
||||
|
||||
elif turn_signal_type == "sequential":
|
||||
for f in signals_path.glob("turn_signal_*"):
|
||||
f.unlink()
|
||||
|
||||
signal_map = {
|
||||
"turnSignal": "turn_signal",
|
||||
"turnSignalBlindspot": "turn_signal_blindspot",
|
||||
}
|
||||
for field, base_name in signal_map.items():
|
||||
file = files.get(field)
|
||||
if file and file.filename:
|
||||
if file.content_length > MAX_FILE_SIZE:
|
||||
return None, f"File {file.filename} exceeds 1MB limit."
|
||||
for f in signals_path.glob(f"{base_name}.*"):
|
||||
f.unlink()
|
||||
ext = Path(file.filename).suffix.lower()
|
||||
file.save(signals_path / f"{base_name}{ext}")
|
||||
|
||||
for f in signals_path.glob("turn_signal.*"):
|
||||
f.unlink()
|
||||
for f in signals_path.glob("turn_signal_blindspot.*"):
|
||||
f.unlink()
|
||||
|
||||
sequential_keys = sorted(
|
||||
[k for k in files if k.startswith("turn_signal_")],
|
||||
key=lambda name: int(name.split("_")[-1])
|
||||
)
|
||||
|
||||
for key in sequential_keys:
|
||||
file = files.get(key)
|
||||
if file and file.filename:
|
||||
if file.content_length > MAX_FILE_SIZE:
|
||||
return None, f"File {file.filename} exceeds 1MB limit."
|
||||
idx = key.split("_")[-1]
|
||||
ext = Path(file.filename).suffix
|
||||
file.save(signals_path / f"turn_signal_{idx}{ext}")
|
||||
|
||||
if save_checklist.get("icons"):
|
||||
(theme_path / "icons").mkdir(exist_ok=True)
|
||||
|
||||
icon_map = {
|
||||
"settingsButton": (theme_path / "icons", "button_settings", (169, 104)),
|
||||
"homeButton": (theme_path / "icons", "button_home", (250, 250)),
|
||||
}
|
||||
|
||||
for field, (dest_path, base_name, resize_dims) in icon_map.items():
|
||||
file = files.get(field)
|
||||
if file and file.filename:
|
||||
if file.content_length > MAX_FILE_SIZE:
|
||||
return None, f"File {file.filename} exceeds 1MB limit."
|
||||
|
||||
for f in dest_path.glob(f"{base_name}.*"):
|
||||
f.unlink()
|
||||
|
||||
ext = Path(file.filename).suffix.lower()
|
||||
save_path = dest_path / f"{base_name}{ext}"
|
||||
file.save(save_path)
|
||||
|
||||
if resize_dims:
|
||||
if ext == ".gif":
|
||||
width, height = resize_dims
|
||||
palette_path = save_path.with_suffix(".palette.png")
|
||||
temp_output_path = save_path.with_suffix(".resized.gif")
|
||||
subprocess.run(["ffmpeg", "-i", str(save_path), "-vf", "palettegen", "-y", str(palette_path)], check=True)
|
||||
subprocess.run(["ffmpeg", "-i", str(save_path), "-i", str(palette_path), "-lavfi", f"fps=20,scale={width}:{height}:flags=lanczos[x];[x][1:v]paletteuse", "-y", str(temp_output_path)], check=True)
|
||||
palette_path.unlink()
|
||||
temp_output_path.rename(save_path)
|
||||
else:
|
||||
img = Image.open(save_path).resize(resize_dims, Image.Resampling.LANCZOS)
|
||||
if ext != ".png":
|
||||
save_path.unlink()
|
||||
save_path = save_path.with_suffix(".png")
|
||||
img.save(save_path, "PNG")
|
||||
|
||||
if save_checklist.get("steering_wheel"):
|
||||
wheels_dir = THEME_SAVE_PATH / "steering_wheels"
|
||||
wheels_dir.mkdir(parents=True, exist_ok=True)
|
||||
file = files.get("steeringWheel")
|
||||
saved_wheel_path = None
|
||||
if file and file.filename:
|
||||
if file.content_length > MAX_FILE_SIZE:
|
||||
return None, f"File {file.filename} exceeds 1MB limit."
|
||||
for f in wheels_dir.glob(f"{sane_theme_name}-user_created.*"):
|
||||
f.unlink()
|
||||
ext = Path(file.filename).suffix.lower()
|
||||
saved_wheel_path = wheels_dir / f"{sane_theme_name}-user_created{ext}"
|
||||
file.save(saved_wheel_path)
|
||||
if ext == ".gif":
|
||||
width, height = (250, 250)
|
||||
palette_path = saved_wheel_path.with_suffix(".palette.png")
|
||||
temp_output_path = saved_wheel_path.with_suffix(".resized.gif")
|
||||
subprocess.run(["ffmpeg", "-i", str(saved_wheel_path), "-vf", "palettegen", "-y", str(palette_path)], check=True)
|
||||
subprocess.run(["ffmpeg", "-i", str(saved_wheel_path), "-i", str(palette_path), "-lavfi", f"fps=20,scale={width}:{height}:flags=lanczos[x];[x][1:v]paletteuse", "-y", str(temp_output_path)], check=True)
|
||||
palette_path.unlink()
|
||||
temp_output_path.rename(saved_wheel_path)
|
||||
else:
|
||||
img = Image.open(saved_wheel_path).resize((250, 250), Image.Resampling.LANCZOS)
|
||||
if ext != ".png":
|
||||
saved_wheel_path.unlink()
|
||||
saved_wheel_path = saved_wheel_path.with_suffix(".png")
|
||||
img.save(saved_wheel_path, "PNG")
|
||||
if temporary and (theme_path is not None):
|
||||
existing = saved_wheel_path if saved_wheel_path is not None else next(wheels_dir.glob(f"{sane_theme_name}-user_created.*"), None)
|
||||
if existing:
|
||||
wheel_icon_dir = theme_path / "WheelIcon"
|
||||
wheel_icon_dir.mkdir(parents=True, exist_ok=True)
|
||||
dest = wheel_icon_dir / f"wheel{existing.suffix.lower()}"
|
||||
if dest.exists():
|
||||
dest.unlink()
|
||||
dest.symlink_to(existing)
|
||||
|
||||
if save_checklist.get("distance_icons"):
|
||||
dist_path = theme_path / "distance_icons"
|
||||
dist_path.mkdir(exist_ok=True)
|
||||
for name in ["traffic", "aggressive", "standard", "relaxed"]:
|
||||
file = files.get(f"distanceIcons_{name}")
|
||||
if file and file.filename:
|
||||
if file.content_length > MAX_FILE_SIZE:
|
||||
return None, f"File {file.filename} exceeds 1MB limit."
|
||||
|
||||
for f in dist_path.glob(f"{name}.*"):
|
||||
f.unlink()
|
||||
|
||||
ext = Path(file.filename).suffix.lower()
|
||||
save_path = dist_path / f"{name}{ext}"
|
||||
file.save(save_path)
|
||||
if ext == ".gif":
|
||||
width, height = (250, 250)
|
||||
palette_path = save_path.with_suffix(".palette.png")
|
||||
temp_output_path = save_path.with_suffix(".resized.gif")
|
||||
subprocess.run(["ffmpeg", "-i", str(save_path), "-vf", "palettegen", "-y", str(palette_path)], check=True)
|
||||
subprocess.run(["ffmpeg", "-i", str(save_path), "-i", str(palette_path), "-lavfi", f"fps=20,scale={width}:{height}:flags=lanczos[x];[x][1:v]paletteuse", "-y", str(temp_output_path)], check=True)
|
||||
palette_path.unlink()
|
||||
temp_output_path.rename(save_path)
|
||||
else:
|
||||
img = Image.open(save_path).resize((250, 250), Image.Resampling.LANCZOS)
|
||||
if ext != ".png":
|
||||
save_path.unlink()
|
||||
save_path = save_path.with_suffix(".png")
|
||||
img.save(save_path, "PNG")
|
||||
|
||||
if save_checklist.get("sounds"):
|
||||
sounds_path = theme_path / "sounds"
|
||||
sounds_path.mkdir(exist_ok=True)
|
||||
for name in ["engage", "disengage", "prompt", "startup"]:
|
||||
file = files.get(name)
|
||||
if file and file.filename:
|
||||
if file.content_length > MAX_FILE_SIZE:
|
||||
return None, f"File {file.filename} exceeds 1MB limit."
|
||||
|
||||
save_path = sounds_path / f"{name}{Path(file.filename).suffix}"
|
||||
file.save(save_path)
|
||||
covert_audio(str(save_path))
|
||||
|
||||
return theme_path, None
|
||||
|
||||
def decode_parameters(encoded_string):
|
||||
obfuscated_data = base64.b64decode(encoded_string.encode("utf-8")).decode("utf-8")
|
||||
decrypted_data = xor_encrypt_decrypt(obfuscated_data, XOR_KEY)
|
||||
@@ -42,26 +334,89 @@ def encode_parameters(params_dict):
|
||||
encoded_data = base64.b64encode(obfuscated_data.encode("utf-8")).decode("utf-8")
|
||||
return encoded_data
|
||||
|
||||
def ffmpeg_concat_segments_to_mp4(input_files, cache_key=None):
|
||||
if not input_files:
|
||||
raise ValueError("No input files provided for concatenation")
|
||||
|
||||
VIDEO_CACHE_PATH.mkdir(exist_ok=True)
|
||||
|
||||
key_str = "|".join(str(p) for p in input_files)
|
||||
if cache_key:
|
||||
key_str = f"{cache_key}|{key_str}"
|
||||
file_hash = hashlib.md5(key_str.encode()).hexdigest()
|
||||
cache_path = VIDEO_CACHE_PATH / f"{file_hash}.mp4"
|
||||
|
||||
if cache_path.exists() and all(cache_path.stat().st_mtime > Path(f).stat().st_mtime for f in input_files):
|
||||
return open(cache_path, "rb")
|
||||
|
||||
list_file = VIDEO_CACHE_PATH / f"{file_hash}.txt"
|
||||
with open(list_file, "w") as f:
|
||||
for seg in input_files:
|
||||
f.write(f"file '{Path(seg)}'\n")
|
||||
|
||||
try:
|
||||
subprocess.run(
|
||||
["ffmpeg", "-hide_banner", "-loglevel", "error", "-f", "concat", "-safe", "0",
|
||||
"-i", str(list_file), "-c", "copy", "-movflags", "faststart", "-y", str(cache_path)],
|
||||
check=True
|
||||
)
|
||||
except subprocess.CalledProcessError:
|
||||
try:
|
||||
subprocess.run(
|
||||
["ffmpeg", "-hide_banner", "-loglevel", "error", "-f", "concat", "-safe", "0",
|
||||
"-i", str(list_file), "-c:v", "libx264", "-movflags", "faststart", "-y", str(cache_path)],
|
||||
check=True
|
||||
)
|
||||
except subprocess.CalledProcessError:
|
||||
if cache_path.exists():
|
||||
cache_path.unlink()
|
||||
raise ValueError(f"Cannot process concatenated video segments: {input_files}")
|
||||
finally:
|
||||
if list_file.exists():
|
||||
list_file.unlink()
|
||||
|
||||
return open(cache_path, "rb")
|
||||
|
||||
def ffmpeg_mp4_wrap_process_builder(filename):
|
||||
is_raw_hevc = filename.rsplit(".", 1)[-1] == "hevc"
|
||||
input_path = Path(filename)
|
||||
|
||||
command = [
|
||||
"ffmpeg",
|
||||
"-hide_banner",
|
||||
"-loglevel", "error",
|
||||
"-probesize", "1M",
|
||||
"-analyzeduration", "1M",
|
||||
*(["-f", "hevc"] if is_raw_hevc else []),
|
||||
"-i", filename,
|
||||
"-c", "copy",
|
||||
"-map", "0",
|
||||
*(["-vtag", "hvc1"] if is_raw_hevc else []),
|
||||
"-f", "mp4",
|
||||
"-movflags", "empty_moov",
|
||||
"-"
|
||||
]
|
||||
if not input_path.exists():
|
||||
raise FileNotFoundError(f"Input file does not exist: {input_path}")
|
||||
|
||||
return subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
|
||||
if input_path.stat().st_size == 0:
|
||||
raise ValueError(f"Input file is empty: {input_path}")
|
||||
|
||||
lock_file = input_path.parent / "rlog.lock"
|
||||
if lock_file.exists():
|
||||
raise ValueError(f"File is still being recorded: {input_path}")
|
||||
|
||||
VIDEO_CACHE_PATH.mkdir(exist_ok=True)
|
||||
|
||||
total, used, free = shutil.disk_usage(VIDEO_CACHE_PATH)
|
||||
if free < 500 * 1024 * 1024:
|
||||
for cache_file in VIDEO_CACHE_PATH.glob("*.mp4"):
|
||||
try:
|
||||
cache_file.unlink()
|
||||
except:
|
||||
pass
|
||||
|
||||
file_hash = hashlib.md5(str(input_path).encode()).hexdigest()
|
||||
cache_path = VIDEO_CACHE_PATH / f"{file_hash}.mp4"
|
||||
|
||||
if cache_path.exists() and cache_path.stat().st_mtime > input_path.stat().st_mtime:
|
||||
return open(cache_path, "rb")
|
||||
|
||||
try:
|
||||
subprocess.run(["ffmpeg", "-hide_banner", "-loglevel", "error", "-i", str(input_path), "-c", "copy", "-movflags", "faststart", "-y", str(cache_path)], check=True)
|
||||
except subprocess.CalledProcessError:
|
||||
try:
|
||||
subprocess.run(["ffmpeg", "-hide_banner", "-loglevel", "error", "-i", str(input_path), "-c:v", "libx264", "-movflags", "faststart", "-y", str(cache_path)], check=True)
|
||||
except subprocess.CalledProcessError:
|
||||
if cache_path.exists():
|
||||
cache_path.unlink()
|
||||
raise ValueError(f"Cannot process video file: {input_path}")
|
||||
|
||||
return open(cache_path, "rb")
|
||||
|
||||
def format_git_date(raw_date: str):
|
||||
date_object = datetime.strptime(raw_date.split()[1], "%Y-%m-%d")
|
||||
@@ -109,15 +464,15 @@ def get_disk_usage():
|
||||
|
||||
def get_drive_stats():
|
||||
stats = json.loads(params.get("ApiCache_DriveStats", encoding="utf-8") or "{}")
|
||||
frogpilot_stats = json.loads(params.get("FrogPilotStats") or "{}")
|
||||
|
||||
is_metric = params.get_bool("IsMetric")
|
||||
conversion = 1 if is_metric else CV.KPH_TO_MPH
|
||||
unit = "kilometers" if is_metric else "miles"
|
||||
|
||||
def process(timeframe):
|
||||
data = stats.get(timeframe, {})
|
||||
return {
|
||||
"distance": data.get("distance", 0) * conversion,
|
||||
"distance": data.get("distance", 0) * (1 if is_metric else CV.KPH_TO_MPH),
|
||||
"drives": data.get("routes", 0),
|
||||
"hours": data.get("minutes", 0) / 60,
|
||||
"unit": unit
|
||||
@@ -126,9 +481,9 @@ def get_drive_stats():
|
||||
stats["all"] = process("all")
|
||||
stats["week"] = process("week")
|
||||
stats["frogpilot"] = {
|
||||
"distance": params_tracking.get_int("FrogPilotKilometers") * conversion,
|
||||
"hours": params_tracking.get_int("FrogPilotMinutes") / 60,
|
||||
"drives": params_tracking.get_int("FrogPilotDrives"),
|
||||
"distance": frogpilot_stats.get("FrogPilotMeters", 0) * (0.001 if is_metric else CV.METER_TO_MILE),
|
||||
"hours": frogpilot_stats.get("FrogPilotSeconds", 0) / (60 * 60),
|
||||
"drives": frogpilot_stats.get("FrogPilotDrives", 0),
|
||||
"unit": unit
|
||||
}
|
||||
|
||||
@@ -173,6 +528,18 @@ def has_preserve_attr(path: str):
|
||||
def list_file(path):
|
||||
return sorted(os.listdir(path), reverse=True)
|
||||
|
||||
def normalize_theme_name(name, for_path=False):
|
||||
name = name.replace("-user_created", "")
|
||||
if for_path:
|
||||
return name.lower().replace(" (", "-").replace(")", "").replace(" ", "-").replace("'", "").replace(".", "")
|
||||
|
||||
parts = re.split(r'[-_]', name)
|
||||
normalized_parts = [part.capitalize() for part in parts]
|
||||
|
||||
if '-' in name and len(normalized_parts) > 1:
|
||||
return f"{normalized_parts[0]} ({' '.join(normalized_parts[1:])})".replace(" Week", "")
|
||||
return ' '.join(normalized_parts).replace(" Week", "")
|
||||
|
||||
def process_route(footage_path, route_name):
|
||||
segment_path = f"{footage_path}{route_name}--0"
|
||||
qcamera_path = f"{segment_path}/qcamera.ts"
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
from .core import contents, where
|
||||
|
||||
__all__ = ["contents", "where"]
|
||||
__version__ = "2025.08.03"
|
||||
@@ -0,0 +1,12 @@
|
||||
import argparse
|
||||
|
||||
from certifi import contents, where
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("-c", "--contents", action="store_true")
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.contents:
|
||||
print(contents())
|
||||
else:
|
||||
print(where())
|
||||
@@ -0,0 +1,83 @@
|
||||
"""
|
||||
certifi.py
|
||||
~~~~~~~~~~
|
||||
|
||||
This module returns the installation location of cacert.pem or its contents.
|
||||
"""
|
||||
import sys
|
||||
import atexit
|
||||
|
||||
def exit_cacert_ctx() -> None:
|
||||
_CACERT_CTX.__exit__(None, None, None) # type: ignore[union-attr]
|
||||
|
||||
|
||||
if sys.version_info >= (3, 11):
|
||||
|
||||
from importlib.resources import as_file, files
|
||||
|
||||
_CACERT_CTX = None
|
||||
_CACERT_PATH = None
|
||||
|
||||
def where() -> str:
|
||||
# This is slightly terrible, but we want to delay extracting the file
|
||||
# in cases where we're inside of a zipimport situation until someone
|
||||
# actually calls where(), but we don't want to re-extract the file
|
||||
# on every call of where(), so we'll do it once then store it in a
|
||||
# global variable.
|
||||
global _CACERT_CTX
|
||||
global _CACERT_PATH
|
||||
if _CACERT_PATH is None:
|
||||
# This is slightly janky, the importlib.resources API wants you to
|
||||
# manage the cleanup of this file, so it doesn't actually return a
|
||||
# path, it returns a context manager that will give you the path
|
||||
# when you enter it and will do any cleanup when you leave it. In
|
||||
# the common case of not needing a temporary file, it will just
|
||||
# return the file system location and the __exit__() is a no-op.
|
||||
#
|
||||
# We also have to hold onto the actual context manager, because
|
||||
# it will do the cleanup whenever it gets garbage collected, so
|
||||
# we will also store that at the global level as well.
|
||||
_CACERT_CTX = as_file(files("certifi").joinpath("cacert.pem"))
|
||||
_CACERT_PATH = str(_CACERT_CTX.__enter__())
|
||||
atexit.register(exit_cacert_ctx)
|
||||
|
||||
return _CACERT_PATH
|
||||
|
||||
def contents() -> str:
|
||||
return files("certifi").joinpath("cacert.pem").read_text(encoding="ascii")
|
||||
|
||||
else:
|
||||
|
||||
from importlib.resources import path as get_path, read_text
|
||||
|
||||
_CACERT_CTX = None
|
||||
_CACERT_PATH = None
|
||||
|
||||
def where() -> str:
|
||||
# This is slightly terrible, but we want to delay extracting the
|
||||
# file in cases where we're inside of a zipimport situation until
|
||||
# someone actually calls where(), but we don't want to re-extract
|
||||
# the file on every call of where(), so we'll do it once then store
|
||||
# it in a global variable.
|
||||
global _CACERT_CTX
|
||||
global _CACERT_PATH
|
||||
if _CACERT_PATH is None:
|
||||
# This is slightly janky, the importlib.resources API wants you
|
||||
# to manage the cleanup of this file, so it doesn't actually
|
||||
# return a path, it returns a context manager that will give
|
||||
# you the path when you enter it and will do any cleanup when
|
||||
# you leave it. In the common case of not needing a temporary
|
||||
# file, it will just return the file system location and the
|
||||
# __exit__() is a no-op.
|
||||
#
|
||||
# We also have to hold onto the actual context manager, because
|
||||
# it will do the cleanup whenever it gets garbage collected, so
|
||||
# we will also store that at the global level as well.
|
||||
_CACERT_CTX = get_path("certifi", "cacert.pem")
|
||||
_CACERT_PATH = str(_CACERT_CTX.__enter__())
|
||||
atexit.register(exit_cacert_ctx)
|
||||
|
||||
return _CACERT_PATH
|
||||
|
||||
def contents() -> str:
|
||||
return read_text("certifi", "cacert.pem", encoding="ascii")
|
||||
@@ -0,0 +1,24 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import sys
|
||||
|
||||
try:
|
||||
from ._version import version as __version__
|
||||
except ImportError:
|
||||
__version__ = 'unknown'
|
||||
|
||||
__all__ = ['easter', 'parser', 'relativedelta', 'rrule', 'tz',
|
||||
'utils', 'zoneinfo']
|
||||
|
||||
def __getattr__(name):
|
||||
import importlib
|
||||
|
||||
if name in __all__:
|
||||
return importlib.import_module("." + name, __name__)
|
||||
raise AttributeError(
|
||||
"module {!r} has not attribute {!r}".format(__name__, name)
|
||||
)
|
||||
|
||||
|
||||
def __dir__():
|
||||
# __dir__ should include all the lazy-importable modules as well.
|
||||
return [x for x in globals() if x not in sys.modules] + __all__
|
||||
@@ -0,0 +1,43 @@
|
||||
"""
|
||||
Common code used in multiple modules.
|
||||
"""
|
||||
|
||||
|
||||
class weekday(object):
|
||||
__slots__ = ["weekday", "n"]
|
||||
|
||||
def __init__(self, weekday, n=None):
|
||||
self.weekday = weekday
|
||||
self.n = n
|
||||
|
||||
def __call__(self, n):
|
||||
if n == self.n:
|
||||
return self
|
||||
else:
|
||||
return self.__class__(self.weekday, n)
|
||||
|
||||
def __eq__(self, other):
|
||||
try:
|
||||
if self.weekday != other.weekday or self.n != other.n:
|
||||
return False
|
||||
except AttributeError:
|
||||
return False
|
||||
return True
|
||||
|
||||
def __hash__(self):
|
||||
return hash((
|
||||
self.weekday,
|
||||
self.n,
|
||||
))
|
||||
|
||||
def __ne__(self, other):
|
||||
return not (self == other)
|
||||
|
||||
def __repr__(self):
|
||||
s = ("MO", "TU", "WE", "TH", "FR", "SA", "SU")[self.weekday]
|
||||
if not self.n:
|
||||
return s
|
||||
else:
|
||||
return "%s(%+d)" % (s, self.n)
|
||||
|
||||
# vim:ts=4:sw=4:et
|
||||
@@ -0,0 +1,4 @@
|
||||
# file generated by setuptools_scm
|
||||
# don't change, don't track in version control
|
||||
__version__ = version = '2.9.0.post0'
|
||||
__version_tuple__ = version_tuple = (2, 9, 0)
|
||||
@@ -0,0 +1,89 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
This module offers a generic Easter computing method for any given year, using
|
||||
Western, Orthodox or Julian algorithms.
|
||||
"""
|
||||
|
||||
import datetime
|
||||
|
||||
__all__ = ["easter", "EASTER_JULIAN", "EASTER_ORTHODOX", "EASTER_WESTERN"]
|
||||
|
||||
EASTER_JULIAN = 1
|
||||
EASTER_ORTHODOX = 2
|
||||
EASTER_WESTERN = 3
|
||||
|
||||
|
||||
def easter(year, method=EASTER_WESTERN):
|
||||
"""
|
||||
This method was ported from the work done by GM Arts,
|
||||
on top of the algorithm by Claus Tondering, which was
|
||||
based in part on the algorithm of Ouding (1940), as
|
||||
quoted in "Explanatory Supplement to the Astronomical
|
||||
Almanac", P. Kenneth Seidelmann, editor.
|
||||
|
||||
This algorithm implements three different Easter
|
||||
calculation methods:
|
||||
|
||||
1. Original calculation in Julian calendar, valid in
|
||||
dates after 326 AD
|
||||
2. Original method, with date converted to Gregorian
|
||||
calendar, valid in years 1583 to 4099
|
||||
3. Revised method, in Gregorian calendar, valid in
|
||||
years 1583 to 4099 as well
|
||||
|
||||
These methods are represented by the constants:
|
||||
|
||||
* ``EASTER_JULIAN = 1``
|
||||
* ``EASTER_ORTHODOX = 2``
|
||||
* ``EASTER_WESTERN = 3``
|
||||
|
||||
The default method is method 3.
|
||||
|
||||
More about the algorithm may be found at:
|
||||
|
||||
`GM Arts: Easter Algorithms <http://www.gmarts.org/index.php?go=415>`_
|
||||
|
||||
and
|
||||
|
||||
`The Calendar FAQ: Easter <https://www.tondering.dk/claus/cal/easter.php>`_
|
||||
|
||||
"""
|
||||
|
||||
if not (1 <= method <= 3):
|
||||
raise ValueError("invalid method")
|
||||
|
||||
# g - Golden year - 1
|
||||
# c - Century
|
||||
# h - (23 - Epact) mod 30
|
||||
# i - Number of days from March 21 to Paschal Full Moon
|
||||
# j - Weekday for PFM (0=Sunday, etc)
|
||||
# p - Number of days from March 21 to Sunday on or before PFM
|
||||
# (-6 to 28 methods 1 & 3, to 56 for method 2)
|
||||
# e - Extra days to add for method 2 (converting Julian
|
||||
# date to Gregorian date)
|
||||
|
||||
y = year
|
||||
g = y % 19
|
||||
e = 0
|
||||
if method < 3:
|
||||
# Old method
|
||||
i = (19*g + 15) % 30
|
||||
j = (y + y//4 + i) % 7
|
||||
if method == 2:
|
||||
# Extra dates to convert Julian to Gregorian date
|
||||
e = 10
|
||||
if y > 1600:
|
||||
e = e + y//100 - 16 - (y//100 - 16)//4
|
||||
else:
|
||||
# New method
|
||||
c = y//100
|
||||
h = (c - c//4 - (8*c + 13)//25 + 19*g + 15) % 30
|
||||
i = h - (h//28)*(1 - (h//28)*(29//(h + 1))*((21 - g)//11))
|
||||
j = (y + y//4 + i + 2 - c + c//4) % 7
|
||||
|
||||
# p can be from -6 to 56 corresponding to dates 22 March to 23 May
|
||||
# (later dates apply to method 2, although 23 May never actually occurs)
|
||||
p = i - j + e
|
||||
d = 1 + (p + 27 + (p + 6)//40) % 31
|
||||
m = 3 + (p + 26)//30
|
||||
return datetime.date(int(y), int(m), int(d))
|
||||
@@ -0,0 +1,61 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from ._parser import parse, parser, parserinfo, ParserError
|
||||
from ._parser import DEFAULTPARSER, DEFAULTTZPARSER
|
||||
from ._parser import UnknownTimezoneWarning
|
||||
|
||||
from ._parser import __doc__
|
||||
|
||||
from .isoparser import isoparser, isoparse
|
||||
|
||||
__all__ = ['parse', 'parser', 'parserinfo',
|
||||
'isoparse', 'isoparser',
|
||||
'ParserError',
|
||||
'UnknownTimezoneWarning']
|
||||
|
||||
|
||||
###
|
||||
# Deprecate portions of the private interface so that downstream code that
|
||||
# is improperly relying on it is given *some* notice.
|
||||
|
||||
|
||||
def __deprecated_private_func(f):
|
||||
from functools import wraps
|
||||
import warnings
|
||||
|
||||
msg = ('{name} is a private function and may break without warning, '
|
||||
'it will be moved and or renamed in future versions.')
|
||||
msg = msg.format(name=f.__name__)
|
||||
|
||||
@wraps(f)
|
||||
def deprecated_func(*args, **kwargs):
|
||||
warnings.warn(msg, DeprecationWarning)
|
||||
return f(*args, **kwargs)
|
||||
|
||||
return deprecated_func
|
||||
|
||||
def __deprecate_private_class(c):
|
||||
import warnings
|
||||
|
||||
msg = ('{name} is a private class and may break without warning, '
|
||||
'it will be moved and or renamed in future versions.')
|
||||
msg = msg.format(name=c.__name__)
|
||||
|
||||
class private_class(c):
|
||||
__doc__ = c.__doc__
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
warnings.warn(msg, DeprecationWarning)
|
||||
super(private_class, self).__init__(*args, **kwargs)
|
||||
|
||||
private_class.__name__ = c.__name__
|
||||
|
||||
return private_class
|
||||
|
||||
|
||||
from ._parser import _timelex, _resultbase
|
||||
from ._parser import _tzparser, _parsetz
|
||||
|
||||
_timelex = __deprecate_private_class(_timelex)
|
||||
_tzparser = __deprecate_private_class(_tzparser)
|
||||
_resultbase = __deprecate_private_class(_resultbase)
|
||||
_parsetz = __deprecated_private_func(_parsetz)
|
||||
@@ -0,0 +1,416 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
This module offers a parser for ISO-8601 strings
|
||||
|
||||
It is intended to support all valid date, time and datetime formats per the
|
||||
ISO-8601 specification.
|
||||
|
||||
..versionadded:: 2.7.0
|
||||
"""
|
||||
from datetime import datetime, timedelta, time, date
|
||||
import calendar
|
||||
from dateutil import tz
|
||||
|
||||
from functools import wraps
|
||||
|
||||
import re
|
||||
import six
|
||||
|
||||
__all__ = ["isoparse", "isoparser"]
|
||||
|
||||
|
||||
def _takes_ascii(f):
|
||||
@wraps(f)
|
||||
def func(self, str_in, *args, **kwargs):
|
||||
# If it's a stream, read the whole thing
|
||||
str_in = getattr(str_in, 'read', lambda: str_in)()
|
||||
|
||||
# If it's unicode, turn it into bytes, since ISO-8601 only covers ASCII
|
||||
if isinstance(str_in, six.text_type):
|
||||
# ASCII is the same in UTF-8
|
||||
try:
|
||||
str_in = str_in.encode('ascii')
|
||||
except UnicodeEncodeError as e:
|
||||
msg = 'ISO-8601 strings should contain only ASCII characters'
|
||||
six.raise_from(ValueError(msg), e)
|
||||
|
||||
return f(self, str_in, *args, **kwargs)
|
||||
|
||||
return func
|
||||
|
||||
|
||||
class isoparser(object):
|
||||
def __init__(self, sep=None):
|
||||
"""
|
||||
:param sep:
|
||||
A single character that separates date and time portions. If
|
||||
``None``, the parser will accept any single character.
|
||||
For strict ISO-8601 adherence, pass ``'T'``.
|
||||
"""
|
||||
if sep is not None:
|
||||
if (len(sep) != 1 or ord(sep) >= 128 or sep in '0123456789'):
|
||||
raise ValueError('Separator must be a single, non-numeric ' +
|
||||
'ASCII character')
|
||||
|
||||
sep = sep.encode('ascii')
|
||||
|
||||
self._sep = sep
|
||||
|
||||
@_takes_ascii
|
||||
def isoparse(self, dt_str):
|
||||
"""
|
||||
Parse an ISO-8601 datetime string into a :class:`datetime.datetime`.
|
||||
|
||||
An ISO-8601 datetime string consists of a date portion, followed
|
||||
optionally by a time portion - the date and time portions are separated
|
||||
by a single character separator, which is ``T`` in the official
|
||||
standard. Incomplete date formats (such as ``YYYY-MM``) may *not* be
|
||||
combined with a time portion.
|
||||
|
||||
Supported date formats are:
|
||||
|
||||
Common:
|
||||
|
||||
- ``YYYY``
|
||||
- ``YYYY-MM``
|
||||
- ``YYYY-MM-DD`` or ``YYYYMMDD``
|
||||
|
||||
Uncommon:
|
||||
|
||||
- ``YYYY-Www`` or ``YYYYWww`` - ISO week (day defaults to 0)
|
||||
- ``YYYY-Www-D`` or ``YYYYWwwD`` - ISO week and day
|
||||
|
||||
The ISO week and day numbering follows the same logic as
|
||||
:func:`datetime.date.isocalendar`.
|
||||
|
||||
Supported time formats are:
|
||||
|
||||
- ``hh``
|
||||
- ``hh:mm`` or ``hhmm``
|
||||
- ``hh:mm:ss`` or ``hhmmss``
|
||||
- ``hh:mm:ss.ssssss`` (Up to 6 sub-second digits)
|
||||
|
||||
Midnight is a special case for `hh`, as the standard supports both
|
||||
00:00 and 24:00 as a representation. The decimal separator can be
|
||||
either a dot or a comma.
|
||||
|
||||
|
||||
.. caution::
|
||||
|
||||
Support for fractional components other than seconds is part of the
|
||||
ISO-8601 standard, but is not currently implemented in this parser.
|
||||
|
||||
Supported time zone offset formats are:
|
||||
|
||||
- `Z` (UTC)
|
||||
- `±HH:MM`
|
||||
- `±HHMM`
|
||||
- `±HH`
|
||||
|
||||
Offsets will be represented as :class:`dateutil.tz.tzoffset` objects,
|
||||
with the exception of UTC, which will be represented as
|
||||
:class:`dateutil.tz.tzutc`. Time zone offsets equivalent to UTC (such
|
||||
as `+00:00`) will also be represented as :class:`dateutil.tz.tzutc`.
|
||||
|
||||
:param dt_str:
|
||||
A string or stream containing only an ISO-8601 datetime string
|
||||
|
||||
:return:
|
||||
Returns a :class:`datetime.datetime` representing the string.
|
||||
Unspecified components default to their lowest value.
|
||||
|
||||
.. warning::
|
||||
|
||||
As of version 2.7.0, the strictness of the parser should not be
|
||||
considered a stable part of the contract. Any valid ISO-8601 string
|
||||
that parses correctly with the default settings will continue to
|
||||
parse correctly in future versions, but invalid strings that
|
||||
currently fail (e.g. ``2017-01-01T00:00+00:00:00``) are not
|
||||
guaranteed to continue failing in future versions if they encode
|
||||
a valid date.
|
||||
|
||||
.. versionadded:: 2.7.0
|
||||
"""
|
||||
components, pos = self._parse_isodate(dt_str)
|
||||
|
||||
if len(dt_str) > pos:
|
||||
if self._sep is None or dt_str[pos:pos + 1] == self._sep:
|
||||
components += self._parse_isotime(dt_str[pos + 1:])
|
||||
else:
|
||||
raise ValueError('String contains unknown ISO components')
|
||||
|
||||
if len(components) > 3 and components[3] == 24:
|
||||
components[3] = 0
|
||||
return datetime(*components) + timedelta(days=1)
|
||||
|
||||
return datetime(*components)
|
||||
|
||||
@_takes_ascii
|
||||
def parse_isodate(self, datestr):
|
||||
"""
|
||||
Parse the date portion of an ISO string.
|
||||
|
||||
:param datestr:
|
||||
The string portion of an ISO string, without a separator
|
||||
|
||||
:return:
|
||||
Returns a :class:`datetime.date` object
|
||||
"""
|
||||
components, pos = self._parse_isodate(datestr)
|
||||
if pos < len(datestr):
|
||||
raise ValueError('String contains unknown ISO ' +
|
||||
'components: {!r}'.format(datestr.decode('ascii')))
|
||||
return date(*components)
|
||||
|
||||
@_takes_ascii
|
||||
def parse_isotime(self, timestr):
|
||||
"""
|
||||
Parse the time portion of an ISO string.
|
||||
|
||||
:param timestr:
|
||||
The time portion of an ISO string, without a separator
|
||||
|
||||
:return:
|
||||
Returns a :class:`datetime.time` object
|
||||
"""
|
||||
components = self._parse_isotime(timestr)
|
||||
if components[0] == 24:
|
||||
components[0] = 0
|
||||
return time(*components)
|
||||
|
||||
@_takes_ascii
|
||||
def parse_tzstr(self, tzstr, zero_as_utc=True):
|
||||
"""
|
||||
Parse a valid ISO time zone string.
|
||||
|
||||
See :func:`isoparser.isoparse` for details on supported formats.
|
||||
|
||||
:param tzstr:
|
||||
A string representing an ISO time zone offset
|
||||
|
||||
:param zero_as_utc:
|
||||
Whether to return :class:`dateutil.tz.tzutc` for zero-offset zones
|
||||
|
||||
:return:
|
||||
Returns :class:`dateutil.tz.tzoffset` for offsets and
|
||||
:class:`dateutil.tz.tzutc` for ``Z`` and (if ``zero_as_utc`` is
|
||||
specified) offsets equivalent to UTC.
|
||||
"""
|
||||
return self._parse_tzstr(tzstr, zero_as_utc=zero_as_utc)
|
||||
|
||||
# Constants
|
||||
_DATE_SEP = b'-'
|
||||
_TIME_SEP = b':'
|
||||
_FRACTION_REGEX = re.compile(b'[\\.,]([0-9]+)')
|
||||
|
||||
def _parse_isodate(self, dt_str):
|
||||
try:
|
||||
return self._parse_isodate_common(dt_str)
|
||||
except ValueError:
|
||||
return self._parse_isodate_uncommon(dt_str)
|
||||
|
||||
def _parse_isodate_common(self, dt_str):
|
||||
len_str = len(dt_str)
|
||||
components = [1, 1, 1]
|
||||
|
||||
if len_str < 4:
|
||||
raise ValueError('ISO string too short')
|
||||
|
||||
# Year
|
||||
components[0] = int(dt_str[0:4])
|
||||
pos = 4
|
||||
if pos >= len_str:
|
||||
return components, pos
|
||||
|
||||
has_sep = dt_str[pos:pos + 1] == self._DATE_SEP
|
||||
if has_sep:
|
||||
pos += 1
|
||||
|
||||
# Month
|
||||
if len_str - pos < 2:
|
||||
raise ValueError('Invalid common month')
|
||||
|
||||
components[1] = int(dt_str[pos:pos + 2])
|
||||
pos += 2
|
||||
|
||||
if pos >= len_str:
|
||||
if has_sep:
|
||||
return components, pos
|
||||
else:
|
||||
raise ValueError('Invalid ISO format')
|
||||
|
||||
if has_sep:
|
||||
if dt_str[pos:pos + 1] != self._DATE_SEP:
|
||||
raise ValueError('Invalid separator in ISO string')
|
||||
pos += 1
|
||||
|
||||
# Day
|
||||
if len_str - pos < 2:
|
||||
raise ValueError('Invalid common day')
|
||||
components[2] = int(dt_str[pos:pos + 2])
|
||||
return components, pos + 2
|
||||
|
||||
def _parse_isodate_uncommon(self, dt_str):
|
||||
if len(dt_str) < 4:
|
||||
raise ValueError('ISO string too short')
|
||||
|
||||
# All ISO formats start with the year
|
||||
year = int(dt_str[0:4])
|
||||
|
||||
has_sep = dt_str[4:5] == self._DATE_SEP
|
||||
|
||||
pos = 4 + has_sep # Skip '-' if it's there
|
||||
if dt_str[pos:pos + 1] == b'W':
|
||||
# YYYY-?Www-?D?
|
||||
pos += 1
|
||||
weekno = int(dt_str[pos:pos + 2])
|
||||
pos += 2
|
||||
|
||||
dayno = 1
|
||||
if len(dt_str) > pos:
|
||||
if (dt_str[pos:pos + 1] == self._DATE_SEP) != has_sep:
|
||||
raise ValueError('Inconsistent use of dash separator')
|
||||
|
||||
pos += has_sep
|
||||
|
||||
dayno = int(dt_str[pos:pos + 1])
|
||||
pos += 1
|
||||
|
||||
base_date = self._calculate_weekdate(year, weekno, dayno)
|
||||
else:
|
||||
# YYYYDDD or YYYY-DDD
|
||||
if len(dt_str) - pos < 3:
|
||||
raise ValueError('Invalid ordinal day')
|
||||
|
||||
ordinal_day = int(dt_str[pos:pos + 3])
|
||||
pos += 3
|
||||
|
||||
if ordinal_day < 1 or ordinal_day > (365 + calendar.isleap(year)):
|
||||
raise ValueError('Invalid ordinal day' +
|
||||
' {} for year {}'.format(ordinal_day, year))
|
||||
|
||||
base_date = date(year, 1, 1) + timedelta(days=ordinal_day - 1)
|
||||
|
||||
components = [base_date.year, base_date.month, base_date.day]
|
||||
return components, pos
|
||||
|
||||
def _calculate_weekdate(self, year, week, day):
|
||||
"""
|
||||
Calculate the day of corresponding to the ISO year-week-day calendar.
|
||||
|
||||
This function is effectively the inverse of
|
||||
:func:`datetime.date.isocalendar`.
|
||||
|
||||
:param year:
|
||||
The year in the ISO calendar
|
||||
|
||||
:param week:
|
||||
The week in the ISO calendar - range is [1, 53]
|
||||
|
||||
:param day:
|
||||
The day in the ISO calendar - range is [1 (MON), 7 (SUN)]
|
||||
|
||||
:return:
|
||||
Returns a :class:`datetime.date`
|
||||
"""
|
||||
if not 0 < week < 54:
|
||||
raise ValueError('Invalid week: {}'.format(week))
|
||||
|
||||
if not 0 < day < 8: # Range is 1-7
|
||||
raise ValueError('Invalid weekday: {}'.format(day))
|
||||
|
||||
# Get week 1 for the specific year:
|
||||
jan_4 = date(year, 1, 4) # Week 1 always has January 4th in it
|
||||
week_1 = jan_4 - timedelta(days=jan_4.isocalendar()[2] - 1)
|
||||
|
||||
# Now add the specific number of weeks and days to get what we want
|
||||
week_offset = (week - 1) * 7 + (day - 1)
|
||||
return week_1 + timedelta(days=week_offset)
|
||||
|
||||
def _parse_isotime(self, timestr):
|
||||
len_str = len(timestr)
|
||||
components = [0, 0, 0, 0, None]
|
||||
pos = 0
|
||||
comp = -1
|
||||
|
||||
if len_str < 2:
|
||||
raise ValueError('ISO time too short')
|
||||
|
||||
has_sep = False
|
||||
|
||||
while pos < len_str and comp < 5:
|
||||
comp += 1
|
||||
|
||||
if timestr[pos:pos + 1] in b'-+Zz':
|
||||
# Detect time zone boundary
|
||||
components[-1] = self._parse_tzstr(timestr[pos:])
|
||||
pos = len_str
|
||||
break
|
||||
|
||||
if comp == 1 and timestr[pos:pos+1] == self._TIME_SEP:
|
||||
has_sep = True
|
||||
pos += 1
|
||||
elif comp == 2 and has_sep:
|
||||
if timestr[pos:pos+1] != self._TIME_SEP:
|
||||
raise ValueError('Inconsistent use of colon separator')
|
||||
pos += 1
|
||||
|
||||
if comp < 3:
|
||||
# Hour, minute, second
|
||||
components[comp] = int(timestr[pos:pos + 2])
|
||||
pos += 2
|
||||
|
||||
if comp == 3:
|
||||
# Fraction of a second
|
||||
frac = self._FRACTION_REGEX.match(timestr[pos:])
|
||||
if not frac:
|
||||
continue
|
||||
|
||||
us_str = frac.group(1)[:6] # Truncate to microseconds
|
||||
components[comp] = int(us_str) * 10**(6 - len(us_str))
|
||||
pos += len(frac.group())
|
||||
|
||||
if pos < len_str:
|
||||
raise ValueError('Unused components in ISO string')
|
||||
|
||||
if components[0] == 24:
|
||||
# Standard supports 00:00 and 24:00 as representations of midnight
|
||||
if any(component != 0 for component in components[1:4]):
|
||||
raise ValueError('Hour may only be 24 at 24:00:00.000')
|
||||
|
||||
return components
|
||||
|
||||
def _parse_tzstr(self, tzstr, zero_as_utc=True):
|
||||
if tzstr == b'Z' or tzstr == b'z':
|
||||
return tz.UTC
|
||||
|
||||
if len(tzstr) not in {3, 5, 6}:
|
||||
raise ValueError('Time zone offset must be 1, 3, 5 or 6 characters')
|
||||
|
||||
if tzstr[0:1] == b'-':
|
||||
mult = -1
|
||||
elif tzstr[0:1] == b'+':
|
||||
mult = 1
|
||||
else:
|
||||
raise ValueError('Time zone offset requires sign')
|
||||
|
||||
hours = int(tzstr[1:3])
|
||||
if len(tzstr) == 3:
|
||||
minutes = 0
|
||||
else:
|
||||
minutes = int(tzstr[(4 if tzstr[3:4] == self._TIME_SEP else 3):])
|
||||
|
||||
if zero_as_utc and hours == 0 and minutes == 0:
|
||||
return tz.UTC
|
||||
else:
|
||||
if minutes > 59:
|
||||
raise ValueError('Invalid minutes in time zone offset')
|
||||
|
||||
if hours > 23:
|
||||
raise ValueError('Invalid hours in time zone offset')
|
||||
|
||||
return tz.tzoffset(None, mult * (hours * 60 + minutes) * 60)
|
||||
|
||||
|
||||
DEFAULT_ISOPARSER = isoparser()
|
||||
isoparse = DEFAULT_ISOPARSER.isoparse
|
||||
@@ -0,0 +1,599 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import datetime
|
||||
import calendar
|
||||
|
||||
import operator
|
||||
from math import copysign
|
||||
|
||||
from six import integer_types
|
||||
from warnings import warn
|
||||
|
||||
from ._common import weekday
|
||||
|
||||
MO, TU, WE, TH, FR, SA, SU = weekdays = tuple(weekday(x) for x in range(7))
|
||||
|
||||
__all__ = ["relativedelta", "MO", "TU", "WE", "TH", "FR", "SA", "SU"]
|
||||
|
||||
|
||||
class relativedelta(object):
|
||||
"""
|
||||
The relativedelta type is designed to be applied to an existing datetime and
|
||||
can replace specific components of that datetime, or represents an interval
|
||||
of time.
|
||||
|
||||
It is based on the specification of the excellent work done by M.-A. Lemburg
|
||||
in his
|
||||
`mx.DateTime <https://www.egenix.com/products/python/mxBase/mxDateTime/>`_ extension.
|
||||
However, notice that this type does *NOT* implement the same algorithm as
|
||||
his work. Do *NOT* expect it to behave like mx.DateTime's counterpart.
|
||||
|
||||
There are two different ways to build a relativedelta instance. The
|
||||
first one is passing it two date/datetime classes::
|
||||
|
||||
relativedelta(datetime1, datetime2)
|
||||
|
||||
The second one is passing it any number of the following keyword arguments::
|
||||
|
||||
relativedelta(arg1=x,arg2=y,arg3=z...)
|
||||
|
||||
year, month, day, hour, minute, second, microsecond:
|
||||
Absolute information (argument is singular); adding or subtracting a
|
||||
relativedelta with absolute information does not perform an arithmetic
|
||||
operation, but rather REPLACES the corresponding value in the
|
||||
original datetime with the value(s) in relativedelta.
|
||||
|
||||
years, months, weeks, days, hours, minutes, seconds, microseconds:
|
||||
Relative information, may be negative (argument is plural); adding
|
||||
or subtracting a relativedelta with relative information performs
|
||||
the corresponding arithmetic operation on the original datetime value
|
||||
with the information in the relativedelta.
|
||||
|
||||
weekday:
|
||||
One of the weekday instances (MO, TU, etc) available in the
|
||||
relativedelta module. These instances may receive a parameter N,
|
||||
specifying the Nth weekday, which could be positive or negative
|
||||
(like MO(+1) or MO(-2)). Not specifying it is the same as specifying
|
||||
+1. You can also use an integer, where 0=MO. This argument is always
|
||||
relative e.g. if the calculated date is already Monday, using MO(1)
|
||||
or MO(-1) won't change the day. To effectively make it absolute, use
|
||||
it in combination with the day argument (e.g. day=1, MO(1) for first
|
||||
Monday of the month).
|
||||
|
||||
leapdays:
|
||||
Will add given days to the date found, if year is a leap
|
||||
year, and the date found is post 28 of february.
|
||||
|
||||
yearday, nlyearday:
|
||||
Set the yearday or the non-leap year day (jump leap days).
|
||||
These are converted to day/month/leapdays information.
|
||||
|
||||
There are relative and absolute forms of the keyword
|
||||
arguments. The plural is relative, and the singular is
|
||||
absolute. For each argument in the order below, the absolute form
|
||||
is applied first (by setting each attribute to that value) and
|
||||
then the relative form (by adding the value to the attribute).
|
||||
|
||||
The order of attributes considered when this relativedelta is
|
||||
added to a datetime is:
|
||||
|
||||
1. Year
|
||||
2. Month
|
||||
3. Day
|
||||
4. Hours
|
||||
5. Minutes
|
||||
6. Seconds
|
||||
7. Microseconds
|
||||
|
||||
Finally, weekday is applied, using the rule described above.
|
||||
|
||||
For example
|
||||
|
||||
>>> from datetime import datetime
|
||||
>>> from dateutil.relativedelta import relativedelta, MO
|
||||
>>> dt = datetime(2018, 4, 9, 13, 37, 0)
|
||||
>>> delta = relativedelta(hours=25, day=1, weekday=MO(1))
|
||||
>>> dt + delta
|
||||
datetime.datetime(2018, 4, 2, 14, 37)
|
||||
|
||||
First, the day is set to 1 (the first of the month), then 25 hours
|
||||
are added, to get to the 2nd day and 14th hour, finally the
|
||||
weekday is applied, but since the 2nd is already a Monday there is
|
||||
no effect.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, dt1=None, dt2=None,
|
||||
years=0, months=0, days=0, leapdays=0, weeks=0,
|
||||
hours=0, minutes=0, seconds=0, microseconds=0,
|
||||
year=None, month=None, day=None, weekday=None,
|
||||
yearday=None, nlyearday=None,
|
||||
hour=None, minute=None, second=None, microsecond=None):
|
||||
|
||||
if dt1 and dt2:
|
||||
# datetime is a subclass of date. So both must be date
|
||||
if not (isinstance(dt1, datetime.date) and
|
||||
isinstance(dt2, datetime.date)):
|
||||
raise TypeError("relativedelta only diffs datetime/date")
|
||||
|
||||
# We allow two dates, or two datetimes, so we coerce them to be
|
||||
# of the same type
|
||||
if (isinstance(dt1, datetime.datetime) !=
|
||||
isinstance(dt2, datetime.datetime)):
|
||||
if not isinstance(dt1, datetime.datetime):
|
||||
dt1 = datetime.datetime.fromordinal(dt1.toordinal())
|
||||
elif not isinstance(dt2, datetime.datetime):
|
||||
dt2 = datetime.datetime.fromordinal(dt2.toordinal())
|
||||
|
||||
self.years = 0
|
||||
self.months = 0
|
||||
self.days = 0
|
||||
self.leapdays = 0
|
||||
self.hours = 0
|
||||
self.minutes = 0
|
||||
self.seconds = 0
|
||||
self.microseconds = 0
|
||||
self.year = None
|
||||
self.month = None
|
||||
self.day = None
|
||||
self.weekday = None
|
||||
self.hour = None
|
||||
self.minute = None
|
||||
self.second = None
|
||||
self.microsecond = None
|
||||
self._has_time = 0
|
||||
|
||||
# Get year / month delta between the two
|
||||
months = (dt1.year - dt2.year) * 12 + (dt1.month - dt2.month)
|
||||
self._set_months(months)
|
||||
|
||||
# Remove the year/month delta so the timedelta is just well-defined
|
||||
# time units (seconds, days and microseconds)
|
||||
dtm = self.__radd__(dt2)
|
||||
|
||||
# If we've overshot our target, make an adjustment
|
||||
if dt1 < dt2:
|
||||
compare = operator.gt
|
||||
increment = 1
|
||||
else:
|
||||
compare = operator.lt
|
||||
increment = -1
|
||||
|
||||
while compare(dt1, dtm):
|
||||
months += increment
|
||||
self._set_months(months)
|
||||
dtm = self.__radd__(dt2)
|
||||
|
||||
# Get the timedelta between the "months-adjusted" date and dt1
|
||||
delta = dt1 - dtm
|
||||
self.seconds = delta.seconds + delta.days * 86400
|
||||
self.microseconds = delta.microseconds
|
||||
else:
|
||||
# Check for non-integer values in integer-only quantities
|
||||
if any(x is not None and x != int(x) for x in (years, months)):
|
||||
raise ValueError("Non-integer years and months are "
|
||||
"ambiguous and not currently supported.")
|
||||
|
||||
# Relative information
|
||||
self.years = int(years)
|
||||
self.months = int(months)
|
||||
self.days = days + weeks * 7
|
||||
self.leapdays = leapdays
|
||||
self.hours = hours
|
||||
self.minutes = minutes
|
||||
self.seconds = seconds
|
||||
self.microseconds = microseconds
|
||||
|
||||
# Absolute information
|
||||
self.year = year
|
||||
self.month = month
|
||||
self.day = day
|
||||
self.hour = hour
|
||||
self.minute = minute
|
||||
self.second = second
|
||||
self.microsecond = microsecond
|
||||
|
||||
if any(x is not None and int(x) != x
|
||||
for x in (year, month, day, hour,
|
||||
minute, second, microsecond)):
|
||||
# For now we'll deprecate floats - later it'll be an error.
|
||||
warn("Non-integer value passed as absolute information. " +
|
||||
"This is not a well-defined condition and will raise " +
|
||||
"errors in future versions.", DeprecationWarning)
|
||||
|
||||
if isinstance(weekday, integer_types):
|
||||
self.weekday = weekdays[weekday]
|
||||
else:
|
||||
self.weekday = weekday
|
||||
|
||||
yday = 0
|
||||
if nlyearday:
|
||||
yday = nlyearday
|
||||
elif yearday:
|
||||
yday = yearday
|
||||
if yearday > 59:
|
||||
self.leapdays = -1
|
||||
if yday:
|
||||
ydayidx = [31, 59, 90, 120, 151, 181, 212,
|
||||
243, 273, 304, 334, 366]
|
||||
for idx, ydays in enumerate(ydayidx):
|
||||
if yday <= ydays:
|
||||
self.month = idx+1
|
||||
if idx == 0:
|
||||
self.day = yday
|
||||
else:
|
||||
self.day = yday-ydayidx[idx-1]
|
||||
break
|
||||
else:
|
||||
raise ValueError("invalid year day (%d)" % yday)
|
||||
|
||||
self._fix()
|
||||
|
||||
def _fix(self):
|
||||
if abs(self.microseconds) > 999999:
|
||||
s = _sign(self.microseconds)
|
||||
div, mod = divmod(self.microseconds * s, 1000000)
|
||||
self.microseconds = mod * s
|
||||
self.seconds += div * s
|
||||
if abs(self.seconds) > 59:
|
||||
s = _sign(self.seconds)
|
||||
div, mod = divmod(self.seconds * s, 60)
|
||||
self.seconds = mod * s
|
||||
self.minutes += div * s
|
||||
if abs(self.minutes) > 59:
|
||||
s = _sign(self.minutes)
|
||||
div, mod = divmod(self.minutes * s, 60)
|
||||
self.minutes = mod * s
|
||||
self.hours += div * s
|
||||
if abs(self.hours) > 23:
|
||||
s = _sign(self.hours)
|
||||
div, mod = divmod(self.hours * s, 24)
|
||||
self.hours = mod * s
|
||||
self.days += div * s
|
||||
if abs(self.months) > 11:
|
||||
s = _sign(self.months)
|
||||
div, mod = divmod(self.months * s, 12)
|
||||
self.months = mod * s
|
||||
self.years += div * s
|
||||
if (self.hours or self.minutes or self.seconds or self.microseconds
|
||||
or self.hour is not None or self.minute is not None or
|
||||
self.second is not None or self.microsecond is not None):
|
||||
self._has_time = 1
|
||||
else:
|
||||
self._has_time = 0
|
||||
|
||||
@property
|
||||
def weeks(self):
|
||||
return int(self.days / 7.0)
|
||||
|
||||
@weeks.setter
|
||||
def weeks(self, value):
|
||||
self.days = self.days - (self.weeks * 7) + value * 7
|
||||
|
||||
def _set_months(self, months):
|
||||
self.months = months
|
||||
if abs(self.months) > 11:
|
||||
s = _sign(self.months)
|
||||
div, mod = divmod(self.months * s, 12)
|
||||
self.months = mod * s
|
||||
self.years = div * s
|
||||
else:
|
||||
self.years = 0
|
||||
|
||||
def normalized(self):
|
||||
"""
|
||||
Return a version of this object represented entirely using integer
|
||||
values for the relative attributes.
|
||||
|
||||
>>> relativedelta(days=1.5, hours=2).normalized()
|
||||
relativedelta(days=+1, hours=+14)
|
||||
|
||||
:return:
|
||||
Returns a :class:`dateutil.relativedelta.relativedelta` object.
|
||||
"""
|
||||
# Cascade remainders down (rounding each to roughly nearest microsecond)
|
||||
days = int(self.days)
|
||||
|
||||
hours_f = round(self.hours + 24 * (self.days - days), 11)
|
||||
hours = int(hours_f)
|
||||
|
||||
minutes_f = round(self.minutes + 60 * (hours_f - hours), 10)
|
||||
minutes = int(minutes_f)
|
||||
|
||||
seconds_f = round(self.seconds + 60 * (minutes_f - minutes), 8)
|
||||
seconds = int(seconds_f)
|
||||
|
||||
microseconds = round(self.microseconds + 1e6 * (seconds_f - seconds))
|
||||
|
||||
# Constructor carries overflow back up with call to _fix()
|
||||
return self.__class__(years=self.years, months=self.months,
|
||||
days=days, hours=hours, minutes=minutes,
|
||||
seconds=seconds, microseconds=microseconds,
|
||||
leapdays=self.leapdays, year=self.year,
|
||||
month=self.month, day=self.day,
|
||||
weekday=self.weekday, hour=self.hour,
|
||||
minute=self.minute, second=self.second,
|
||||
microsecond=self.microsecond)
|
||||
|
||||
def __add__(self, other):
|
||||
if isinstance(other, relativedelta):
|
||||
return self.__class__(years=other.years + self.years,
|
||||
months=other.months + self.months,
|
||||
days=other.days + self.days,
|
||||
hours=other.hours + self.hours,
|
||||
minutes=other.minutes + self.minutes,
|
||||
seconds=other.seconds + self.seconds,
|
||||
microseconds=(other.microseconds +
|
||||
self.microseconds),
|
||||
leapdays=other.leapdays or self.leapdays,
|
||||
year=(other.year if other.year is not None
|
||||
else self.year),
|
||||
month=(other.month if other.month is not None
|
||||
else self.month),
|
||||
day=(other.day if other.day is not None
|
||||
else self.day),
|
||||
weekday=(other.weekday if other.weekday is not None
|
||||
else self.weekday),
|
||||
hour=(other.hour if other.hour is not None
|
||||
else self.hour),
|
||||
minute=(other.minute if other.minute is not None
|
||||
else self.minute),
|
||||
second=(other.second if other.second is not None
|
||||
else self.second),
|
||||
microsecond=(other.microsecond if other.microsecond
|
||||
is not None else
|
||||
self.microsecond))
|
||||
if isinstance(other, datetime.timedelta):
|
||||
return self.__class__(years=self.years,
|
||||
months=self.months,
|
||||
days=self.days + other.days,
|
||||
hours=self.hours,
|
||||
minutes=self.minutes,
|
||||
seconds=self.seconds + other.seconds,
|
||||
microseconds=self.microseconds + other.microseconds,
|
||||
leapdays=self.leapdays,
|
||||
year=self.year,
|
||||
month=self.month,
|
||||
day=self.day,
|
||||
weekday=self.weekday,
|
||||
hour=self.hour,
|
||||
minute=self.minute,
|
||||
second=self.second,
|
||||
microsecond=self.microsecond)
|
||||
if not isinstance(other, datetime.date):
|
||||
return NotImplemented
|
||||
elif self._has_time and not isinstance(other, datetime.datetime):
|
||||
other = datetime.datetime.fromordinal(other.toordinal())
|
||||
year = (self.year or other.year)+self.years
|
||||
month = self.month or other.month
|
||||
if self.months:
|
||||
assert 1 <= abs(self.months) <= 12
|
||||
month += self.months
|
||||
if month > 12:
|
||||
year += 1
|
||||
month -= 12
|
||||
elif month < 1:
|
||||
year -= 1
|
||||
month += 12
|
||||
day = min(calendar.monthrange(year, month)[1],
|
||||
self.day or other.day)
|
||||
repl = {"year": year, "month": month, "day": day}
|
||||
for attr in ["hour", "minute", "second", "microsecond"]:
|
||||
value = getattr(self, attr)
|
||||
if value is not None:
|
||||
repl[attr] = value
|
||||
days = self.days
|
||||
if self.leapdays and month > 2 and calendar.isleap(year):
|
||||
days += self.leapdays
|
||||
ret = (other.replace(**repl)
|
||||
+ datetime.timedelta(days=days,
|
||||
hours=self.hours,
|
||||
minutes=self.minutes,
|
||||
seconds=self.seconds,
|
||||
microseconds=self.microseconds))
|
||||
if self.weekday:
|
||||
weekday, nth = self.weekday.weekday, self.weekday.n or 1
|
||||
jumpdays = (abs(nth) - 1) * 7
|
||||
if nth > 0:
|
||||
jumpdays += (7 - ret.weekday() + weekday) % 7
|
||||
else:
|
||||
jumpdays += (ret.weekday() - weekday) % 7
|
||||
jumpdays *= -1
|
||||
ret += datetime.timedelta(days=jumpdays)
|
||||
return ret
|
||||
|
||||
def __radd__(self, other):
|
||||
return self.__add__(other)
|
||||
|
||||
def __rsub__(self, other):
|
||||
return self.__neg__().__radd__(other)
|
||||
|
||||
def __sub__(self, other):
|
||||
if not isinstance(other, relativedelta):
|
||||
return NotImplemented # In case the other object defines __rsub__
|
||||
return self.__class__(years=self.years - other.years,
|
||||
months=self.months - other.months,
|
||||
days=self.days - other.days,
|
||||
hours=self.hours - other.hours,
|
||||
minutes=self.minutes - other.minutes,
|
||||
seconds=self.seconds - other.seconds,
|
||||
microseconds=self.microseconds - other.microseconds,
|
||||
leapdays=self.leapdays or other.leapdays,
|
||||
year=(self.year if self.year is not None
|
||||
else other.year),
|
||||
month=(self.month if self.month is not None else
|
||||
other.month),
|
||||
day=(self.day if self.day is not None else
|
||||
other.day),
|
||||
weekday=(self.weekday if self.weekday is not None else
|
||||
other.weekday),
|
||||
hour=(self.hour if self.hour is not None else
|
||||
other.hour),
|
||||
minute=(self.minute if self.minute is not None else
|
||||
other.minute),
|
||||
second=(self.second if self.second is not None else
|
||||
other.second),
|
||||
microsecond=(self.microsecond if self.microsecond
|
||||
is not None else
|
||||
other.microsecond))
|
||||
|
||||
def __abs__(self):
|
||||
return self.__class__(years=abs(self.years),
|
||||
months=abs(self.months),
|
||||
days=abs(self.days),
|
||||
hours=abs(self.hours),
|
||||
minutes=abs(self.minutes),
|
||||
seconds=abs(self.seconds),
|
||||
microseconds=abs(self.microseconds),
|
||||
leapdays=self.leapdays,
|
||||
year=self.year,
|
||||
month=self.month,
|
||||
day=self.day,
|
||||
weekday=self.weekday,
|
||||
hour=self.hour,
|
||||
minute=self.minute,
|
||||
second=self.second,
|
||||
microsecond=self.microsecond)
|
||||
|
||||
def __neg__(self):
|
||||
return self.__class__(years=-self.years,
|
||||
months=-self.months,
|
||||
days=-self.days,
|
||||
hours=-self.hours,
|
||||
minutes=-self.minutes,
|
||||
seconds=-self.seconds,
|
||||
microseconds=-self.microseconds,
|
||||
leapdays=self.leapdays,
|
||||
year=self.year,
|
||||
month=self.month,
|
||||
day=self.day,
|
||||
weekday=self.weekday,
|
||||
hour=self.hour,
|
||||
minute=self.minute,
|
||||
second=self.second,
|
||||
microsecond=self.microsecond)
|
||||
|
||||
def __bool__(self):
|
||||
return not (not self.years and
|
||||
not self.months and
|
||||
not self.days and
|
||||
not self.hours and
|
||||
not self.minutes and
|
||||
not self.seconds and
|
||||
not self.microseconds and
|
||||
not self.leapdays and
|
||||
self.year is None and
|
||||
self.month is None and
|
||||
self.day is None and
|
||||
self.weekday is None and
|
||||
self.hour is None and
|
||||
self.minute is None and
|
||||
self.second is None and
|
||||
self.microsecond is None)
|
||||
# Compatibility with Python 2.x
|
||||
__nonzero__ = __bool__
|
||||
|
||||
def __mul__(self, other):
|
||||
try:
|
||||
f = float(other)
|
||||
except TypeError:
|
||||
return NotImplemented
|
||||
|
||||
return self.__class__(years=int(self.years * f),
|
||||
months=int(self.months * f),
|
||||
days=int(self.days * f),
|
||||
hours=int(self.hours * f),
|
||||
minutes=int(self.minutes * f),
|
||||
seconds=int(self.seconds * f),
|
||||
microseconds=int(self.microseconds * f),
|
||||
leapdays=self.leapdays,
|
||||
year=self.year,
|
||||
month=self.month,
|
||||
day=self.day,
|
||||
weekday=self.weekday,
|
||||
hour=self.hour,
|
||||
minute=self.minute,
|
||||
second=self.second,
|
||||
microsecond=self.microsecond)
|
||||
|
||||
__rmul__ = __mul__
|
||||
|
||||
def __eq__(self, other):
|
||||
if not isinstance(other, relativedelta):
|
||||
return NotImplemented
|
||||
if self.weekday or other.weekday:
|
||||
if not self.weekday or not other.weekday:
|
||||
return False
|
||||
if self.weekday.weekday != other.weekday.weekday:
|
||||
return False
|
||||
n1, n2 = self.weekday.n, other.weekday.n
|
||||
if n1 != n2 and not ((not n1 or n1 == 1) and (not n2 or n2 == 1)):
|
||||
return False
|
||||
return (self.years == other.years and
|
||||
self.months == other.months and
|
||||
self.days == other.days and
|
||||
self.hours == other.hours and
|
||||
self.minutes == other.minutes and
|
||||
self.seconds == other.seconds and
|
||||
self.microseconds == other.microseconds and
|
||||
self.leapdays == other.leapdays and
|
||||
self.year == other.year and
|
||||
self.month == other.month and
|
||||
self.day == other.day and
|
||||
self.hour == other.hour and
|
||||
self.minute == other.minute and
|
||||
self.second == other.second and
|
||||
self.microsecond == other.microsecond)
|
||||
|
||||
def __hash__(self):
|
||||
return hash((
|
||||
self.weekday,
|
||||
self.years,
|
||||
self.months,
|
||||
self.days,
|
||||
self.hours,
|
||||
self.minutes,
|
||||
self.seconds,
|
||||
self.microseconds,
|
||||
self.leapdays,
|
||||
self.year,
|
||||
self.month,
|
||||
self.day,
|
||||
self.hour,
|
||||
self.minute,
|
||||
self.second,
|
||||
self.microsecond,
|
||||
))
|
||||
|
||||
def __ne__(self, other):
|
||||
return not self.__eq__(other)
|
||||
|
||||
def __div__(self, other):
|
||||
try:
|
||||
reciprocal = 1 / float(other)
|
||||
except TypeError:
|
||||
return NotImplemented
|
||||
|
||||
return self.__mul__(reciprocal)
|
||||
|
||||
__truediv__ = __div__
|
||||
|
||||
def __repr__(self):
|
||||
l = []
|
||||
for attr in ["years", "months", "days", "leapdays",
|
||||
"hours", "minutes", "seconds", "microseconds"]:
|
||||
value = getattr(self, attr)
|
||||
if value:
|
||||
l.append("{attr}={value:+g}".format(attr=attr, value=value))
|
||||
for attr in ["year", "month", "day", "weekday",
|
||||
"hour", "minute", "second", "microsecond"]:
|
||||
value = getattr(self, attr)
|
||||
if value is not None:
|
||||
l.append("{attr}={value}".format(attr=attr, value=repr(value)))
|
||||
return "{classname}({attrs})".format(classname=self.__class__.__name__,
|
||||
attrs=", ".join(l))
|
||||
|
||||
|
||||
def _sign(x):
|
||||
return int(copysign(1, x))
|
||||
|
||||
# vim:ts=4:sw=4:et
|
||||
@@ -0,0 +1,12 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from .tz import *
|
||||
from .tz import __doc__
|
||||
|
||||
__all__ = ["tzutc", "tzoffset", "tzlocal", "tzfile", "tzrange",
|
||||
"tzstr", "tzical", "tzwin", "tzwinlocal", "gettz",
|
||||
"enfold", "datetime_ambiguous", "datetime_exists",
|
||||
"resolve_imaginary", "UTC", "DeprecatedTzFormatWarning"]
|
||||
|
||||
|
||||
class DeprecatedTzFormatWarning(Warning):
|
||||
"""Warning raised when time zones are parsed from deprecated formats."""
|
||||
@@ -0,0 +1,419 @@
|
||||
from six import PY2
|
||||
|
||||
from functools import wraps
|
||||
|
||||
from datetime import datetime, timedelta, tzinfo
|
||||
|
||||
|
||||
ZERO = timedelta(0)
|
||||
|
||||
__all__ = ['tzname_in_python2', 'enfold']
|
||||
|
||||
|
||||
def tzname_in_python2(namefunc):
|
||||
"""Change unicode output into bytestrings in Python 2
|
||||
|
||||
tzname() API changed in Python 3. It used to return bytes, but was changed
|
||||
to unicode strings
|
||||
"""
|
||||
if PY2:
|
||||
@wraps(namefunc)
|
||||
def adjust_encoding(*args, **kwargs):
|
||||
name = namefunc(*args, **kwargs)
|
||||
if name is not None:
|
||||
name = name.encode()
|
||||
|
||||
return name
|
||||
|
||||
return adjust_encoding
|
||||
else:
|
||||
return namefunc
|
||||
|
||||
|
||||
# The following is adapted from Alexander Belopolsky's tz library
|
||||
# https://github.com/abalkin/tz
|
||||
if hasattr(datetime, 'fold'):
|
||||
# This is the pre-python 3.6 fold situation
|
||||
def enfold(dt, fold=1):
|
||||
"""
|
||||
Provides a unified interface for assigning the ``fold`` attribute to
|
||||
datetimes both before and after the implementation of PEP-495.
|
||||
|
||||
:param fold:
|
||||
The value for the ``fold`` attribute in the returned datetime. This
|
||||
should be either 0 or 1.
|
||||
|
||||
:return:
|
||||
Returns an object for which ``getattr(dt, 'fold', 0)`` returns
|
||||
``fold`` for all versions of Python. In versions prior to
|
||||
Python 3.6, this is a ``_DatetimeWithFold`` object, which is a
|
||||
subclass of :py:class:`datetime.datetime` with the ``fold``
|
||||
attribute added, if ``fold`` is 1.
|
||||
|
||||
.. versionadded:: 2.6.0
|
||||
"""
|
||||
return dt.replace(fold=fold)
|
||||
|
||||
else:
|
||||
class _DatetimeWithFold(datetime):
|
||||
"""
|
||||
This is a class designed to provide a PEP 495-compliant interface for
|
||||
Python versions before 3.6. It is used only for dates in a fold, so
|
||||
the ``fold`` attribute is fixed at ``1``.
|
||||
|
||||
.. versionadded:: 2.6.0
|
||||
"""
|
||||
__slots__ = ()
|
||||
|
||||
def replace(self, *args, **kwargs):
|
||||
"""
|
||||
Return a datetime with the same attributes, except for those
|
||||
attributes given new values by whichever keyword arguments are
|
||||
specified. Note that tzinfo=None can be specified to create a naive
|
||||
datetime from an aware datetime with no conversion of date and time
|
||||
data.
|
||||
|
||||
This is reimplemented in ``_DatetimeWithFold`` because pypy3 will
|
||||
return a ``datetime.datetime`` even if ``fold`` is unchanged.
|
||||
"""
|
||||
argnames = (
|
||||
'year', 'month', 'day', 'hour', 'minute', 'second',
|
||||
'microsecond', 'tzinfo'
|
||||
)
|
||||
|
||||
for arg, argname in zip(args, argnames):
|
||||
if argname in kwargs:
|
||||
raise TypeError('Duplicate argument: {}'.format(argname))
|
||||
|
||||
kwargs[argname] = arg
|
||||
|
||||
for argname in argnames:
|
||||
if argname not in kwargs:
|
||||
kwargs[argname] = getattr(self, argname)
|
||||
|
||||
dt_class = self.__class__ if kwargs.get('fold', 1) else datetime
|
||||
|
||||
return dt_class(**kwargs)
|
||||
|
||||
@property
|
||||
def fold(self):
|
||||
return 1
|
||||
|
||||
def enfold(dt, fold=1):
|
||||
"""
|
||||
Provides a unified interface for assigning the ``fold`` attribute to
|
||||
datetimes both before and after the implementation of PEP-495.
|
||||
|
||||
:param fold:
|
||||
The value for the ``fold`` attribute in the returned datetime. This
|
||||
should be either 0 or 1.
|
||||
|
||||
:return:
|
||||
Returns an object for which ``getattr(dt, 'fold', 0)`` returns
|
||||
``fold`` for all versions of Python. In versions prior to
|
||||
Python 3.6, this is a ``_DatetimeWithFold`` object, which is a
|
||||
subclass of :py:class:`datetime.datetime` with the ``fold``
|
||||
attribute added, if ``fold`` is 1.
|
||||
|
||||
.. versionadded:: 2.6.0
|
||||
"""
|
||||
if getattr(dt, 'fold', 0) == fold:
|
||||
return dt
|
||||
|
||||
args = dt.timetuple()[:6]
|
||||
args += (dt.microsecond, dt.tzinfo)
|
||||
|
||||
if fold:
|
||||
return _DatetimeWithFold(*args)
|
||||
else:
|
||||
return datetime(*args)
|
||||
|
||||
|
||||
def _validate_fromutc_inputs(f):
|
||||
"""
|
||||
The CPython version of ``fromutc`` checks that the input is a ``datetime``
|
||||
object and that ``self`` is attached as its ``tzinfo``.
|
||||
"""
|
||||
@wraps(f)
|
||||
def fromutc(self, dt):
|
||||
if not isinstance(dt, datetime):
|
||||
raise TypeError("fromutc() requires a datetime argument")
|
||||
if dt.tzinfo is not self:
|
||||
raise ValueError("dt.tzinfo is not self")
|
||||
|
||||
return f(self, dt)
|
||||
|
||||
return fromutc
|
||||
|
||||
|
||||
class _tzinfo(tzinfo):
|
||||
"""
|
||||
Base class for all ``dateutil`` ``tzinfo`` objects.
|
||||
"""
|
||||
|
||||
def is_ambiguous(self, dt):
|
||||
"""
|
||||
Whether or not the "wall time" of a given datetime is ambiguous in this
|
||||
zone.
|
||||
|
||||
:param dt:
|
||||
A :py:class:`datetime.datetime`, naive or time zone aware.
|
||||
|
||||
|
||||
:return:
|
||||
Returns ``True`` if ambiguous, ``False`` otherwise.
|
||||
|
||||
.. versionadded:: 2.6.0
|
||||
"""
|
||||
|
||||
dt = dt.replace(tzinfo=self)
|
||||
|
||||
wall_0 = enfold(dt, fold=0)
|
||||
wall_1 = enfold(dt, fold=1)
|
||||
|
||||
same_offset = wall_0.utcoffset() == wall_1.utcoffset()
|
||||
same_dt = wall_0.replace(tzinfo=None) == wall_1.replace(tzinfo=None)
|
||||
|
||||
return same_dt and not same_offset
|
||||
|
||||
def _fold_status(self, dt_utc, dt_wall):
|
||||
"""
|
||||
Determine the fold status of a "wall" datetime, given a representation
|
||||
of the same datetime as a (naive) UTC datetime. This is calculated based
|
||||
on the assumption that ``dt.utcoffset() - dt.dst()`` is constant for all
|
||||
datetimes, and that this offset is the actual number of hours separating
|
||||
``dt_utc`` and ``dt_wall``.
|
||||
|
||||
:param dt_utc:
|
||||
Representation of the datetime as UTC
|
||||
|
||||
:param dt_wall:
|
||||
Representation of the datetime as "wall time". This parameter must
|
||||
either have a `fold` attribute or have a fold-naive
|
||||
:class:`datetime.tzinfo` attached, otherwise the calculation may
|
||||
fail.
|
||||
"""
|
||||
if self.is_ambiguous(dt_wall):
|
||||
delta_wall = dt_wall - dt_utc
|
||||
_fold = int(delta_wall == (dt_utc.utcoffset() - dt_utc.dst()))
|
||||
else:
|
||||
_fold = 0
|
||||
|
||||
return _fold
|
||||
|
||||
def _fold(self, dt):
|
||||
return getattr(dt, 'fold', 0)
|
||||
|
||||
def _fromutc(self, dt):
|
||||
"""
|
||||
Given a timezone-aware datetime in a given timezone, calculates a
|
||||
timezone-aware datetime in a new timezone.
|
||||
|
||||
Since this is the one time that we *know* we have an unambiguous
|
||||
datetime object, we take this opportunity to determine whether the
|
||||
datetime is ambiguous and in a "fold" state (e.g. if it's the first
|
||||
occurrence, chronologically, of the ambiguous datetime).
|
||||
|
||||
:param dt:
|
||||
A timezone-aware :class:`datetime.datetime` object.
|
||||
"""
|
||||
|
||||
# Re-implement the algorithm from Python's datetime.py
|
||||
dtoff = dt.utcoffset()
|
||||
if dtoff is None:
|
||||
raise ValueError("fromutc() requires a non-None utcoffset() "
|
||||
"result")
|
||||
|
||||
# The original datetime.py code assumes that `dst()` defaults to
|
||||
# zero during ambiguous times. PEP 495 inverts this presumption, so
|
||||
# for pre-PEP 495 versions of python, we need to tweak the algorithm.
|
||||
dtdst = dt.dst()
|
||||
if dtdst is None:
|
||||
raise ValueError("fromutc() requires a non-None dst() result")
|
||||
delta = dtoff - dtdst
|
||||
|
||||
dt += delta
|
||||
# Set fold=1 so we can default to being in the fold for
|
||||
# ambiguous dates.
|
||||
dtdst = enfold(dt, fold=1).dst()
|
||||
if dtdst is None:
|
||||
raise ValueError("fromutc(): dt.dst gave inconsistent "
|
||||
"results; cannot convert")
|
||||
return dt + dtdst
|
||||
|
||||
@_validate_fromutc_inputs
|
||||
def fromutc(self, dt):
|
||||
"""
|
||||
Given a timezone-aware datetime in a given timezone, calculates a
|
||||
timezone-aware datetime in a new timezone.
|
||||
|
||||
Since this is the one time that we *know* we have an unambiguous
|
||||
datetime object, we take this opportunity to determine whether the
|
||||
datetime is ambiguous and in a "fold" state (e.g. if it's the first
|
||||
occurrence, chronologically, of the ambiguous datetime).
|
||||
|
||||
:param dt:
|
||||
A timezone-aware :class:`datetime.datetime` object.
|
||||
"""
|
||||
dt_wall = self._fromutc(dt)
|
||||
|
||||
# Calculate the fold status given the two datetimes.
|
||||
_fold = self._fold_status(dt, dt_wall)
|
||||
|
||||
# Set the default fold value for ambiguous dates
|
||||
return enfold(dt_wall, fold=_fold)
|
||||
|
||||
|
||||
class tzrangebase(_tzinfo):
|
||||
"""
|
||||
This is an abstract base class for time zones represented by an annual
|
||||
transition into and out of DST. Child classes should implement the following
|
||||
methods:
|
||||
|
||||
* ``__init__(self, *args, **kwargs)``
|
||||
* ``transitions(self, year)`` - this is expected to return a tuple of
|
||||
datetimes representing the DST on and off transitions in standard
|
||||
time.
|
||||
|
||||
A fully initialized ``tzrangebase`` subclass should also provide the
|
||||
following attributes:
|
||||
* ``hasdst``: Boolean whether or not the zone uses DST.
|
||||
* ``_dst_offset`` / ``_std_offset``: :class:`datetime.timedelta` objects
|
||||
representing the respective UTC offsets.
|
||||
* ``_dst_abbr`` / ``_std_abbr``: Strings representing the timezone short
|
||||
abbreviations in DST and STD, respectively.
|
||||
* ``_hasdst``: Whether or not the zone has DST.
|
||||
|
||||
.. versionadded:: 2.6.0
|
||||
"""
|
||||
def __init__(self):
|
||||
raise NotImplementedError('tzrangebase is an abstract base class')
|
||||
|
||||
def utcoffset(self, dt):
|
||||
isdst = self._isdst(dt)
|
||||
|
||||
if isdst is None:
|
||||
return None
|
||||
elif isdst:
|
||||
return self._dst_offset
|
||||
else:
|
||||
return self._std_offset
|
||||
|
||||
def dst(self, dt):
|
||||
isdst = self._isdst(dt)
|
||||
|
||||
if isdst is None:
|
||||
return None
|
||||
elif isdst:
|
||||
return self._dst_base_offset
|
||||
else:
|
||||
return ZERO
|
||||
|
||||
@tzname_in_python2
|
||||
def tzname(self, dt):
|
||||
if self._isdst(dt):
|
||||
return self._dst_abbr
|
||||
else:
|
||||
return self._std_abbr
|
||||
|
||||
def fromutc(self, dt):
|
||||
""" Given a datetime in UTC, return local time """
|
||||
if not isinstance(dt, datetime):
|
||||
raise TypeError("fromutc() requires a datetime argument")
|
||||
|
||||
if dt.tzinfo is not self:
|
||||
raise ValueError("dt.tzinfo is not self")
|
||||
|
||||
# Get transitions - if there are none, fixed offset
|
||||
transitions = self.transitions(dt.year)
|
||||
if transitions is None:
|
||||
return dt + self.utcoffset(dt)
|
||||
|
||||
# Get the transition times in UTC
|
||||
dston, dstoff = transitions
|
||||
|
||||
dston -= self._std_offset
|
||||
dstoff -= self._std_offset
|
||||
|
||||
utc_transitions = (dston, dstoff)
|
||||
dt_utc = dt.replace(tzinfo=None)
|
||||
|
||||
isdst = self._naive_isdst(dt_utc, utc_transitions)
|
||||
|
||||
if isdst:
|
||||
dt_wall = dt + self._dst_offset
|
||||
else:
|
||||
dt_wall = dt + self._std_offset
|
||||
|
||||
_fold = int(not isdst and self.is_ambiguous(dt_wall))
|
||||
|
||||
return enfold(dt_wall, fold=_fold)
|
||||
|
||||
def is_ambiguous(self, dt):
|
||||
"""
|
||||
Whether or not the "wall time" of a given datetime is ambiguous in this
|
||||
zone.
|
||||
|
||||
:param dt:
|
||||
A :py:class:`datetime.datetime`, naive or time zone aware.
|
||||
|
||||
|
||||
:return:
|
||||
Returns ``True`` if ambiguous, ``False`` otherwise.
|
||||
|
||||
.. versionadded:: 2.6.0
|
||||
"""
|
||||
if not self.hasdst:
|
||||
return False
|
||||
|
||||
start, end = self.transitions(dt.year)
|
||||
|
||||
dt = dt.replace(tzinfo=None)
|
||||
return (end <= dt < end + self._dst_base_offset)
|
||||
|
||||
def _isdst(self, dt):
|
||||
if not self.hasdst:
|
||||
return False
|
||||
elif dt is None:
|
||||
return None
|
||||
|
||||
transitions = self.transitions(dt.year)
|
||||
|
||||
if transitions is None:
|
||||
return False
|
||||
|
||||
dt = dt.replace(tzinfo=None)
|
||||
|
||||
isdst = self._naive_isdst(dt, transitions)
|
||||
|
||||
# Handle ambiguous dates
|
||||
if not isdst and self.is_ambiguous(dt):
|
||||
return not self._fold(dt)
|
||||
else:
|
||||
return isdst
|
||||
|
||||
def _naive_isdst(self, dt, transitions):
|
||||
dston, dstoff = transitions
|
||||
|
||||
dt = dt.replace(tzinfo=None)
|
||||
|
||||
if dston < dstoff:
|
||||
isdst = dston <= dt < dstoff
|
||||
else:
|
||||
isdst = not dstoff <= dt < dston
|
||||
|
||||
return isdst
|
||||
|
||||
@property
|
||||
def _dst_base_offset(self):
|
||||
return self._dst_offset - self._std_offset
|
||||
|
||||
__hash__ = None
|
||||
|
||||
def __ne__(self, other):
|
||||
return not (self == other)
|
||||
|
||||
def __repr__(self):
|
||||
return "%s(...)" % self.__class__.__name__
|
||||
|
||||
__reduce__ = object.__reduce__
|
||||
@@ -0,0 +1,80 @@
|
||||
from datetime import timedelta
|
||||
import weakref
|
||||
from collections import OrderedDict
|
||||
|
||||
from six.moves import _thread
|
||||
|
||||
|
||||
class _TzSingleton(type):
|
||||
def __init__(cls, *args, **kwargs):
|
||||
cls.__instance = None
|
||||
super(_TzSingleton, cls).__init__(*args, **kwargs)
|
||||
|
||||
def __call__(cls):
|
||||
if cls.__instance is None:
|
||||
cls.__instance = super(_TzSingleton, cls).__call__()
|
||||
return cls.__instance
|
||||
|
||||
|
||||
class _TzFactory(type):
|
||||
def instance(cls, *args, **kwargs):
|
||||
"""Alternate constructor that returns a fresh instance"""
|
||||
return type.__call__(cls, *args, **kwargs)
|
||||
|
||||
|
||||
class _TzOffsetFactory(_TzFactory):
|
||||
def __init__(cls, *args, **kwargs):
|
||||
cls.__instances = weakref.WeakValueDictionary()
|
||||
cls.__strong_cache = OrderedDict()
|
||||
cls.__strong_cache_size = 8
|
||||
|
||||
cls._cache_lock = _thread.allocate_lock()
|
||||
|
||||
def __call__(cls, name, offset):
|
||||
if isinstance(offset, timedelta):
|
||||
key = (name, offset.total_seconds())
|
||||
else:
|
||||
key = (name, offset)
|
||||
|
||||
instance = cls.__instances.get(key, None)
|
||||
if instance is None:
|
||||
instance = cls.__instances.setdefault(key,
|
||||
cls.instance(name, offset))
|
||||
|
||||
# This lock may not be necessary in Python 3. See GH issue #901
|
||||
with cls._cache_lock:
|
||||
cls.__strong_cache[key] = cls.__strong_cache.pop(key, instance)
|
||||
|
||||
# Remove an item if the strong cache is overpopulated
|
||||
if len(cls.__strong_cache) > cls.__strong_cache_size:
|
||||
cls.__strong_cache.popitem(last=False)
|
||||
|
||||
return instance
|
||||
|
||||
|
||||
class _TzStrFactory(_TzFactory):
|
||||
def __init__(cls, *args, **kwargs):
|
||||
cls.__instances = weakref.WeakValueDictionary()
|
||||
cls.__strong_cache = OrderedDict()
|
||||
cls.__strong_cache_size = 8
|
||||
|
||||
cls.__cache_lock = _thread.allocate_lock()
|
||||
|
||||
def __call__(cls, s, posix_offset=False):
|
||||
key = (s, posix_offset)
|
||||
instance = cls.__instances.get(key, None)
|
||||
|
||||
if instance is None:
|
||||
instance = cls.__instances.setdefault(key,
|
||||
cls.instance(s, posix_offset))
|
||||
|
||||
# This lock may not be necessary in Python 3. See GH issue #901
|
||||
with cls.__cache_lock:
|
||||
cls.__strong_cache[key] = cls.__strong_cache.pop(key, instance)
|
||||
|
||||
# Remove an item if the strong cache is overpopulated
|
||||
if len(cls.__strong_cache) > cls.__strong_cache_size:
|
||||
cls.__strong_cache.popitem(last=False)
|
||||
|
||||
return instance
|
||||
|
||||
@@ -0,0 +1,370 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
This module provides an interface to the native time zone data on Windows,
|
||||
including :py:class:`datetime.tzinfo` implementations.
|
||||
|
||||
Attempting to import this module on a non-Windows platform will raise an
|
||||
:py:obj:`ImportError`.
|
||||
"""
|
||||
# This code was originally contributed by Jeffrey Harris.
|
||||
import datetime
|
||||
import struct
|
||||
|
||||
from six.moves import winreg
|
||||
from six import text_type
|
||||
|
||||
try:
|
||||
import ctypes
|
||||
from ctypes import wintypes
|
||||
except ValueError:
|
||||
# ValueError is raised on non-Windows systems for some horrible reason.
|
||||
raise ImportError("Running tzwin on non-Windows system")
|
||||
|
||||
from ._common import tzrangebase
|
||||
|
||||
__all__ = ["tzwin", "tzwinlocal", "tzres"]
|
||||
|
||||
ONEWEEK = datetime.timedelta(7)
|
||||
|
||||
TZKEYNAMENT = r"SOFTWARE\Microsoft\Windows NT\CurrentVersion\Time Zones"
|
||||
TZKEYNAME9X = r"SOFTWARE\Microsoft\Windows\CurrentVersion\Time Zones"
|
||||
TZLOCALKEYNAME = r"SYSTEM\CurrentControlSet\Control\TimeZoneInformation"
|
||||
|
||||
|
||||
def _settzkeyname():
|
||||
handle = winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE)
|
||||
try:
|
||||
winreg.OpenKey(handle, TZKEYNAMENT).Close()
|
||||
TZKEYNAME = TZKEYNAMENT
|
||||
except WindowsError:
|
||||
TZKEYNAME = TZKEYNAME9X
|
||||
handle.Close()
|
||||
return TZKEYNAME
|
||||
|
||||
|
||||
TZKEYNAME = _settzkeyname()
|
||||
|
||||
|
||||
class tzres(object):
|
||||
"""
|
||||
Class for accessing ``tzres.dll``, which contains timezone name related
|
||||
resources.
|
||||
|
||||
.. versionadded:: 2.5.0
|
||||
"""
|
||||
p_wchar = ctypes.POINTER(wintypes.WCHAR) # Pointer to a wide char
|
||||
|
||||
def __init__(self, tzres_loc='tzres.dll'):
|
||||
# Load the user32 DLL so we can load strings from tzres
|
||||
user32 = ctypes.WinDLL('user32')
|
||||
|
||||
# Specify the LoadStringW function
|
||||
user32.LoadStringW.argtypes = (wintypes.HINSTANCE,
|
||||
wintypes.UINT,
|
||||
wintypes.LPWSTR,
|
||||
ctypes.c_int)
|
||||
|
||||
self.LoadStringW = user32.LoadStringW
|
||||
self._tzres = ctypes.WinDLL(tzres_loc)
|
||||
self.tzres_loc = tzres_loc
|
||||
|
||||
def load_name(self, offset):
|
||||
"""
|
||||
Load a timezone name from a DLL offset (integer).
|
||||
|
||||
>>> from dateutil.tzwin import tzres
|
||||
>>> tzr = tzres()
|
||||
>>> print(tzr.load_name(112))
|
||||
'Eastern Standard Time'
|
||||
|
||||
:param offset:
|
||||
A positive integer value referring to a string from the tzres dll.
|
||||
|
||||
.. note::
|
||||
|
||||
Offsets found in the registry are generally of the form
|
||||
``@tzres.dll,-114``. The offset in this case is 114, not -114.
|
||||
|
||||
"""
|
||||
resource = self.p_wchar()
|
||||
lpBuffer = ctypes.cast(ctypes.byref(resource), wintypes.LPWSTR)
|
||||
nchar = self.LoadStringW(self._tzres._handle, offset, lpBuffer, 0)
|
||||
return resource[:nchar]
|
||||
|
||||
def name_from_string(self, tzname_str):
|
||||
"""
|
||||
Parse strings as returned from the Windows registry into the time zone
|
||||
name as defined in the registry.
|
||||
|
||||
>>> from dateutil.tzwin import tzres
|
||||
>>> tzr = tzres()
|
||||
>>> print(tzr.name_from_string('@tzres.dll,-251'))
|
||||
'Dateline Daylight Time'
|
||||
>>> print(tzr.name_from_string('Eastern Standard Time'))
|
||||
'Eastern Standard Time'
|
||||
|
||||
:param tzname_str:
|
||||
A timezone name string as returned from a Windows registry key.
|
||||
|
||||
:return:
|
||||
Returns the localized timezone string from tzres.dll if the string
|
||||
is of the form `@tzres.dll,-offset`, else returns the input string.
|
||||
"""
|
||||
if not tzname_str.startswith('@'):
|
||||
return tzname_str
|
||||
|
||||
name_splt = tzname_str.split(',-')
|
||||
try:
|
||||
offset = int(name_splt[1])
|
||||
except:
|
||||
raise ValueError("Malformed timezone string.")
|
||||
|
||||
return self.load_name(offset)
|
||||
|
||||
|
||||
class tzwinbase(tzrangebase):
|
||||
"""tzinfo class based on win32's timezones available in the registry."""
|
||||
def __init__(self):
|
||||
raise NotImplementedError('tzwinbase is an abstract base class')
|
||||
|
||||
def __eq__(self, other):
|
||||
# Compare on all relevant dimensions, including name.
|
||||
if not isinstance(other, tzwinbase):
|
||||
return NotImplemented
|
||||
|
||||
return (self._std_offset == other._std_offset and
|
||||
self._dst_offset == other._dst_offset and
|
||||
self._stddayofweek == other._stddayofweek and
|
||||
self._dstdayofweek == other._dstdayofweek and
|
||||
self._stdweeknumber == other._stdweeknumber and
|
||||
self._dstweeknumber == other._dstweeknumber and
|
||||
self._stdhour == other._stdhour and
|
||||
self._dsthour == other._dsthour and
|
||||
self._stdminute == other._stdminute and
|
||||
self._dstminute == other._dstminute and
|
||||
self._std_abbr == other._std_abbr and
|
||||
self._dst_abbr == other._dst_abbr)
|
||||
|
||||
@staticmethod
|
||||
def list():
|
||||
"""Return a list of all time zones known to the system."""
|
||||
with winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE) as handle:
|
||||
with winreg.OpenKey(handle, TZKEYNAME) as tzkey:
|
||||
result = [winreg.EnumKey(tzkey, i)
|
||||
for i in range(winreg.QueryInfoKey(tzkey)[0])]
|
||||
return result
|
||||
|
||||
def display(self):
|
||||
"""
|
||||
Return the display name of the time zone.
|
||||
"""
|
||||
return self._display
|
||||
|
||||
def transitions(self, year):
|
||||
"""
|
||||
For a given year, get the DST on and off transition times, expressed
|
||||
always on the standard time side. For zones with no transitions, this
|
||||
function returns ``None``.
|
||||
|
||||
:param year:
|
||||
The year whose transitions you would like to query.
|
||||
|
||||
:return:
|
||||
Returns a :class:`tuple` of :class:`datetime.datetime` objects,
|
||||
``(dston, dstoff)`` for zones with an annual DST transition, or
|
||||
``None`` for fixed offset zones.
|
||||
"""
|
||||
|
||||
if not self.hasdst:
|
||||
return None
|
||||
|
||||
dston = picknthweekday(year, self._dstmonth, self._dstdayofweek,
|
||||
self._dsthour, self._dstminute,
|
||||
self._dstweeknumber)
|
||||
|
||||
dstoff = picknthweekday(year, self._stdmonth, self._stddayofweek,
|
||||
self._stdhour, self._stdminute,
|
||||
self._stdweeknumber)
|
||||
|
||||
# Ambiguous dates default to the STD side
|
||||
dstoff -= self._dst_base_offset
|
||||
|
||||
return dston, dstoff
|
||||
|
||||
def _get_hasdst(self):
|
||||
return self._dstmonth != 0
|
||||
|
||||
@property
|
||||
def _dst_base_offset(self):
|
||||
return self._dst_base_offset_
|
||||
|
||||
|
||||
class tzwin(tzwinbase):
|
||||
"""
|
||||
Time zone object created from the zone info in the Windows registry
|
||||
|
||||
These are similar to :py:class:`dateutil.tz.tzrange` objects in that
|
||||
the time zone data is provided in the format of a single offset rule
|
||||
for either 0 or 2 time zone transitions per year.
|
||||
|
||||
:param: name
|
||||
The name of a Windows time zone key, e.g. "Eastern Standard Time".
|
||||
The full list of keys can be retrieved with :func:`tzwin.list`.
|
||||
"""
|
||||
|
||||
def __init__(self, name):
|
||||
self._name = name
|
||||
|
||||
with winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE) as handle:
|
||||
tzkeyname = text_type("{kn}\\{name}").format(kn=TZKEYNAME, name=name)
|
||||
with winreg.OpenKey(handle, tzkeyname) as tzkey:
|
||||
keydict = valuestodict(tzkey)
|
||||
|
||||
self._std_abbr = keydict["Std"]
|
||||
self._dst_abbr = keydict["Dlt"]
|
||||
|
||||
self._display = keydict["Display"]
|
||||
|
||||
# See http://ww_winreg.jsiinc.com/SUBA/tip0300/rh0398.htm
|
||||
tup = struct.unpack("=3l16h", keydict["TZI"])
|
||||
stdoffset = -tup[0]-tup[1] # Bias + StandardBias * -1
|
||||
dstoffset = stdoffset-tup[2] # + DaylightBias * -1
|
||||
self._std_offset = datetime.timedelta(minutes=stdoffset)
|
||||
self._dst_offset = datetime.timedelta(minutes=dstoffset)
|
||||
|
||||
# for the meaning see the win32 TIME_ZONE_INFORMATION structure docs
|
||||
# http://msdn.microsoft.com/en-us/library/windows/desktop/ms725481(v=vs.85).aspx
|
||||
(self._stdmonth,
|
||||
self._stddayofweek, # Sunday = 0
|
||||
self._stdweeknumber, # Last = 5
|
||||
self._stdhour,
|
||||
self._stdminute) = tup[4:9]
|
||||
|
||||
(self._dstmonth,
|
||||
self._dstdayofweek, # Sunday = 0
|
||||
self._dstweeknumber, # Last = 5
|
||||
self._dsthour,
|
||||
self._dstminute) = tup[12:17]
|
||||
|
||||
self._dst_base_offset_ = self._dst_offset - self._std_offset
|
||||
self.hasdst = self._get_hasdst()
|
||||
|
||||
def __repr__(self):
|
||||
return "tzwin(%s)" % repr(self._name)
|
||||
|
||||
def __reduce__(self):
|
||||
return (self.__class__, (self._name,))
|
||||
|
||||
|
||||
class tzwinlocal(tzwinbase):
|
||||
"""
|
||||
Class representing the local time zone information in the Windows registry
|
||||
|
||||
While :class:`dateutil.tz.tzlocal` makes system calls (via the :mod:`time`
|
||||
module) to retrieve time zone information, ``tzwinlocal`` retrieves the
|
||||
rules directly from the Windows registry and creates an object like
|
||||
:class:`dateutil.tz.tzwin`.
|
||||
|
||||
Because Windows does not have an equivalent of :func:`time.tzset`, on
|
||||
Windows, :class:`dateutil.tz.tzlocal` instances will always reflect the
|
||||
time zone settings *at the time that the process was started*, meaning
|
||||
changes to the machine's time zone settings during the run of a program
|
||||
on Windows will **not** be reflected by :class:`dateutil.tz.tzlocal`.
|
||||
Because ``tzwinlocal`` reads the registry directly, it is unaffected by
|
||||
this issue.
|
||||
"""
|
||||
def __init__(self):
|
||||
with winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE) as handle:
|
||||
with winreg.OpenKey(handle, TZLOCALKEYNAME) as tzlocalkey:
|
||||
keydict = valuestodict(tzlocalkey)
|
||||
|
||||
self._std_abbr = keydict["StandardName"]
|
||||
self._dst_abbr = keydict["DaylightName"]
|
||||
|
||||
try:
|
||||
tzkeyname = text_type('{kn}\\{sn}').format(kn=TZKEYNAME,
|
||||
sn=self._std_abbr)
|
||||
with winreg.OpenKey(handle, tzkeyname) as tzkey:
|
||||
_keydict = valuestodict(tzkey)
|
||||
self._display = _keydict["Display"]
|
||||
except OSError:
|
||||
self._display = None
|
||||
|
||||
stdoffset = -keydict["Bias"]-keydict["StandardBias"]
|
||||
dstoffset = stdoffset-keydict["DaylightBias"]
|
||||
|
||||
self._std_offset = datetime.timedelta(minutes=stdoffset)
|
||||
self._dst_offset = datetime.timedelta(minutes=dstoffset)
|
||||
|
||||
# For reasons unclear, in this particular key, the day of week has been
|
||||
# moved to the END of the SYSTEMTIME structure.
|
||||
tup = struct.unpack("=8h", keydict["StandardStart"])
|
||||
|
||||
(self._stdmonth,
|
||||
self._stdweeknumber, # Last = 5
|
||||
self._stdhour,
|
||||
self._stdminute) = tup[1:5]
|
||||
|
||||
self._stddayofweek = tup[7]
|
||||
|
||||
tup = struct.unpack("=8h", keydict["DaylightStart"])
|
||||
|
||||
(self._dstmonth,
|
||||
self._dstweeknumber, # Last = 5
|
||||
self._dsthour,
|
||||
self._dstminute) = tup[1:5]
|
||||
|
||||
self._dstdayofweek = tup[7]
|
||||
|
||||
self._dst_base_offset_ = self._dst_offset - self._std_offset
|
||||
self.hasdst = self._get_hasdst()
|
||||
|
||||
def __repr__(self):
|
||||
return "tzwinlocal()"
|
||||
|
||||
def __str__(self):
|
||||
# str will return the standard name, not the daylight name.
|
||||
return "tzwinlocal(%s)" % repr(self._std_abbr)
|
||||
|
||||
def __reduce__(self):
|
||||
return (self.__class__, ())
|
||||
|
||||
|
||||
def picknthweekday(year, month, dayofweek, hour, minute, whichweek):
|
||||
""" dayofweek == 0 means Sunday, whichweek 5 means last instance """
|
||||
first = datetime.datetime(year, month, 1, hour, minute)
|
||||
|
||||
# This will work if dayofweek is ISO weekday (1-7) or Microsoft-style (0-6),
|
||||
# Because 7 % 7 = 0
|
||||
weekdayone = first.replace(day=((dayofweek - first.isoweekday()) % 7) + 1)
|
||||
wd = weekdayone + ((whichweek - 1) * ONEWEEK)
|
||||
if (wd.month != month):
|
||||
wd -= ONEWEEK
|
||||
|
||||
return wd
|
||||
|
||||
|
||||
def valuestodict(key):
|
||||
"""Convert a registry key's values to a dictionary."""
|
||||
dout = {}
|
||||
size = winreg.QueryInfoKey(key)[1]
|
||||
tz_res = None
|
||||
|
||||
for i in range(size):
|
||||
key_name, value, dtype = winreg.EnumValue(key, i)
|
||||
if dtype == winreg.REG_DWORD or dtype == winreg.REG_DWORD_LITTLE_ENDIAN:
|
||||
# If it's a DWORD (32-bit integer), it's stored as unsigned - convert
|
||||
# that to a proper signed integer
|
||||
if value & (1 << 31):
|
||||
value = value - (1 << 32)
|
||||
elif dtype == winreg.REG_SZ:
|
||||
# If it's a reference to the tzres DLL, load the actual string
|
||||
if value.startswith('@tzres'):
|
||||
tz_res = tz_res or tzres()
|
||||
value = tz_res.name_from_string(value)
|
||||
|
||||
value = value.rstrip('\x00') # Remove trailing nulls
|
||||
|
||||
dout[key_name] = value
|
||||
|
||||
return dout
|
||||
@@ -0,0 +1,2 @@
|
||||
# tzwin has moved to dateutil.tz.win
|
||||
from .tz.win import *
|
||||
@@ -0,0 +1,71 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
This module offers general convenience and utility functions for dealing with
|
||||
datetimes.
|
||||
|
||||
.. versionadded:: 2.7.0
|
||||
"""
|
||||
from __future__ import unicode_literals
|
||||
|
||||
from datetime import datetime, time
|
||||
|
||||
|
||||
def today(tzinfo=None):
|
||||
"""
|
||||
Returns a :py:class:`datetime` representing the current day at midnight
|
||||
|
||||
:param tzinfo:
|
||||
The time zone to attach (also used to determine the current day).
|
||||
|
||||
:return:
|
||||
A :py:class:`datetime.datetime` object representing the current day
|
||||
at midnight.
|
||||
"""
|
||||
|
||||
dt = datetime.now(tzinfo)
|
||||
return datetime.combine(dt.date(), time(0, tzinfo=tzinfo))
|
||||
|
||||
|
||||
def default_tzinfo(dt, tzinfo):
|
||||
"""
|
||||
Sets the ``tzinfo`` parameter on naive datetimes only
|
||||
|
||||
This is useful for example when you are provided a datetime that may have
|
||||
either an implicit or explicit time zone, such as when parsing a time zone
|
||||
string.
|
||||
|
||||
.. doctest::
|
||||
|
||||
>>> from dateutil.tz import tzoffset
|
||||
>>> from dateutil.parser import parse
|
||||
>>> from dateutil.utils import default_tzinfo
|
||||
>>> dflt_tz = tzoffset("EST", -18000)
|
||||
>>> print(default_tzinfo(parse('2014-01-01 12:30 UTC'), dflt_tz))
|
||||
2014-01-01 12:30:00+00:00
|
||||
>>> print(default_tzinfo(parse('2014-01-01 12:30'), dflt_tz))
|
||||
2014-01-01 12:30:00-05:00
|
||||
|
||||
:param dt:
|
||||
The datetime on which to replace the time zone
|
||||
|
||||
:param tzinfo:
|
||||
The :py:class:`datetime.tzinfo` subclass instance to assign to
|
||||
``dt`` if (and only if) it is naive.
|
||||
|
||||
:return:
|
||||
Returns an aware :py:class:`datetime.datetime`.
|
||||
"""
|
||||
if dt.tzinfo is not None:
|
||||
return dt
|
||||
else:
|
||||
return dt.replace(tzinfo=tzinfo)
|
||||
|
||||
|
||||
def within_delta(dt1, dt2, delta):
|
||||
"""
|
||||
Useful for comparing two datetimes that may have a negligible difference
|
||||
to be considered equal.
|
||||
"""
|
||||
delta = abs(delta)
|
||||
difference = dt1 - dt2
|
||||
return -delta <= difference <= delta
|
||||
@@ -0,0 +1,167 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
import warnings
|
||||
import json
|
||||
|
||||
from tarfile import TarFile
|
||||
from pkgutil import get_data
|
||||
from io import BytesIO
|
||||
|
||||
from dateutil.tz import tzfile as _tzfile
|
||||
|
||||
__all__ = ["get_zonefile_instance", "gettz", "gettz_db_metadata"]
|
||||
|
||||
ZONEFILENAME = "dateutil-zoneinfo.tar.gz"
|
||||
METADATA_FN = 'METADATA'
|
||||
|
||||
|
||||
class tzfile(_tzfile):
|
||||
def __reduce__(self):
|
||||
return (gettz, (self._filename,))
|
||||
|
||||
|
||||
def getzoneinfofile_stream():
|
||||
try:
|
||||
return BytesIO(get_data(__name__, ZONEFILENAME))
|
||||
except IOError as e: # TODO switch to FileNotFoundError?
|
||||
warnings.warn("I/O error({0}): {1}".format(e.errno, e.strerror))
|
||||
return None
|
||||
|
||||
|
||||
class ZoneInfoFile(object):
|
||||
def __init__(self, zonefile_stream=None):
|
||||
if zonefile_stream is not None:
|
||||
with TarFile.open(fileobj=zonefile_stream) as tf:
|
||||
self.zones = {zf.name: tzfile(tf.extractfile(zf), filename=zf.name)
|
||||
for zf in tf.getmembers()
|
||||
if zf.isfile() and zf.name != METADATA_FN}
|
||||
# deal with links: They'll point to their parent object. Less
|
||||
# waste of memory
|
||||
links = {zl.name: self.zones[zl.linkname]
|
||||
for zl in tf.getmembers() if
|
||||
zl.islnk() or zl.issym()}
|
||||
self.zones.update(links)
|
||||
try:
|
||||
metadata_json = tf.extractfile(tf.getmember(METADATA_FN))
|
||||
metadata_str = metadata_json.read().decode('UTF-8')
|
||||
self.metadata = json.loads(metadata_str)
|
||||
except KeyError:
|
||||
# no metadata in tar file
|
||||
self.metadata = None
|
||||
else:
|
||||
self.zones = {}
|
||||
self.metadata = None
|
||||
|
||||
def get(self, name, default=None):
|
||||
"""
|
||||
Wrapper for :func:`ZoneInfoFile.zones.get`. This is a convenience method
|
||||
for retrieving zones from the zone dictionary.
|
||||
|
||||
:param name:
|
||||
The name of the zone to retrieve. (Generally IANA zone names)
|
||||
|
||||
:param default:
|
||||
The value to return in the event of a missing key.
|
||||
|
||||
.. versionadded:: 2.6.0
|
||||
|
||||
"""
|
||||
return self.zones.get(name, default)
|
||||
|
||||
|
||||
# The current API has gettz as a module function, although in fact it taps into
|
||||
# a stateful class. So as a workaround for now, without changing the API, we
|
||||
# will create a new "global" class instance the first time a user requests a
|
||||
# timezone. Ugly, but adheres to the api.
|
||||
#
|
||||
# TODO: Remove after deprecation period.
|
||||
_CLASS_ZONE_INSTANCE = []
|
||||
|
||||
|
||||
def get_zonefile_instance(new_instance=False):
|
||||
"""
|
||||
This is a convenience function which provides a :class:`ZoneInfoFile`
|
||||
instance using the data provided by the ``dateutil`` package. By default, it
|
||||
caches a single instance of the ZoneInfoFile object and returns that.
|
||||
|
||||
:param new_instance:
|
||||
If ``True``, a new instance of :class:`ZoneInfoFile` is instantiated and
|
||||
used as the cached instance for the next call. Otherwise, new instances
|
||||
are created only as necessary.
|
||||
|
||||
:return:
|
||||
Returns a :class:`ZoneInfoFile` object.
|
||||
|
||||
.. versionadded:: 2.6
|
||||
"""
|
||||
if new_instance:
|
||||
zif = None
|
||||
else:
|
||||
zif = getattr(get_zonefile_instance, '_cached_instance', None)
|
||||
|
||||
if zif is None:
|
||||
zif = ZoneInfoFile(getzoneinfofile_stream())
|
||||
|
||||
get_zonefile_instance._cached_instance = zif
|
||||
|
||||
return zif
|
||||
|
||||
|
||||
def gettz(name):
|
||||
"""
|
||||
This retrieves a time zone from the local zoneinfo tarball that is packaged
|
||||
with dateutil.
|
||||
|
||||
:param name:
|
||||
An IANA-style time zone name, as found in the zoneinfo file.
|
||||
|
||||
:return:
|
||||
Returns a :class:`dateutil.tz.tzfile` time zone object.
|
||||
|
||||
.. warning::
|
||||
It is generally inadvisable to use this function, and it is only
|
||||
provided for API compatibility with earlier versions. This is *not*
|
||||
equivalent to ``dateutil.tz.gettz()``, which selects an appropriate
|
||||
time zone based on the inputs, favoring system zoneinfo. This is ONLY
|
||||
for accessing the dateutil-specific zoneinfo (which may be out of
|
||||
date compared to the system zoneinfo).
|
||||
|
||||
.. deprecated:: 2.6
|
||||
If you need to use a specific zoneinfofile over the system zoneinfo,
|
||||
instantiate a :class:`dateutil.zoneinfo.ZoneInfoFile` object and call
|
||||
:func:`dateutil.zoneinfo.ZoneInfoFile.get(name)` instead.
|
||||
|
||||
Use :func:`get_zonefile_instance` to retrieve an instance of the
|
||||
dateutil-provided zoneinfo.
|
||||
"""
|
||||
warnings.warn("zoneinfo.gettz() will be removed in future versions, "
|
||||
"to use the dateutil-provided zoneinfo files, instantiate a "
|
||||
"ZoneInfoFile object and use ZoneInfoFile.zones.get() "
|
||||
"instead. See the documentation for details.",
|
||||
DeprecationWarning)
|
||||
|
||||
if len(_CLASS_ZONE_INSTANCE) == 0:
|
||||
_CLASS_ZONE_INSTANCE.append(ZoneInfoFile(getzoneinfofile_stream()))
|
||||
return _CLASS_ZONE_INSTANCE[0].zones.get(name)
|
||||
|
||||
|
||||
def gettz_db_metadata():
|
||||
""" Get the zonefile metadata
|
||||
|
||||
See `zonefile_metadata`_
|
||||
|
||||
:returns:
|
||||
A dictionary with the database metadata
|
||||
|
||||
.. deprecated:: 2.6
|
||||
See deprecation warning in :func:`zoneinfo.gettz`. To get metadata,
|
||||
query the attribute ``zoneinfo.ZoneInfoFile.metadata``.
|
||||
"""
|
||||
warnings.warn("zoneinfo.gettz_db_metadata() will be removed in future "
|
||||
"versions, to use the dateutil-provided zoneinfo files, "
|
||||
"ZoneInfoFile object and query the 'metadata' attribute "
|
||||
"instead. See the documentation for details.",
|
||||
DeprecationWarning)
|
||||
|
||||
if len(_CLASS_ZONE_INSTANCE) == 0:
|
||||
_CLASS_ZONE_INSTANCE.append(ZoneInfoFile(getzoneinfofile_stream()))
|
||||
return _CLASS_ZONE_INSTANCE[0].metadata
|
||||
@@ -0,0 +1,75 @@
|
||||
import logging
|
||||
import os
|
||||
import tempfile
|
||||
import shutil
|
||||
import json
|
||||
from subprocess import check_call, check_output
|
||||
from tarfile import TarFile
|
||||
|
||||
from dateutil.zoneinfo import METADATA_FN, ZONEFILENAME
|
||||
|
||||
|
||||
def rebuild(filename, tag=None, format="gz", zonegroups=[], metadata=None):
|
||||
"""Rebuild the internal timezone info in dateutil/zoneinfo/zoneinfo*tar*
|
||||
|
||||
filename is the timezone tarball from ``ftp.iana.org/tz``.
|
||||
|
||||
"""
|
||||
tmpdir = tempfile.mkdtemp()
|
||||
zonedir = os.path.join(tmpdir, "zoneinfo")
|
||||
moduledir = os.path.dirname(__file__)
|
||||
try:
|
||||
with TarFile.open(filename) as tf:
|
||||
for name in zonegroups:
|
||||
tf.extract(name, tmpdir)
|
||||
filepaths = [os.path.join(tmpdir, n) for n in zonegroups]
|
||||
|
||||
_run_zic(zonedir, filepaths)
|
||||
|
||||
# write metadata file
|
||||
with open(os.path.join(zonedir, METADATA_FN), 'w') as f:
|
||||
json.dump(metadata, f, indent=4, sort_keys=True)
|
||||
target = os.path.join(moduledir, ZONEFILENAME)
|
||||
with TarFile.open(target, "w:%s" % format) as tf:
|
||||
for entry in os.listdir(zonedir):
|
||||
entrypath = os.path.join(zonedir, entry)
|
||||
tf.add(entrypath, entry)
|
||||
finally:
|
||||
shutil.rmtree(tmpdir)
|
||||
|
||||
|
||||
def _run_zic(zonedir, filepaths):
|
||||
"""Calls the ``zic`` compiler in a compatible way to get a "fat" binary.
|
||||
|
||||
Recent versions of ``zic`` default to ``-b slim``, while older versions
|
||||
don't even have the ``-b`` option (but default to "fat" binaries). The
|
||||
current version of dateutil does not support Version 2+ TZif files, which
|
||||
causes problems when used in conjunction with "slim" binaries, so this
|
||||
function is used to ensure that we always get a "fat" binary.
|
||||
"""
|
||||
|
||||
try:
|
||||
help_text = check_output(["zic", "--help"])
|
||||
except OSError as e:
|
||||
_print_on_nosuchfile(e)
|
||||
raise
|
||||
|
||||
if b"-b " in help_text:
|
||||
bloat_args = ["-b", "fat"]
|
||||
else:
|
||||
bloat_args = []
|
||||
|
||||
check_call(["zic"] + bloat_args + ["-d", zonedir] + filepaths)
|
||||
|
||||
|
||||
def _print_on_nosuchfile(e):
|
||||
"""Print helpful troubleshooting message
|
||||
|
||||
e is an exception raised by subprocess.check_call()
|
||||
|
||||
"""
|
||||
if e.errno == 2:
|
||||
logging.error(
|
||||
"Could not find zic. Perhaps you need to install "
|
||||
"libc-bin or some other package that provides it, "
|
||||
"or it's not in your PATH?")
|
||||
@@ -0,0 +1,396 @@
|
||||
# coding: utf-8
|
||||
|
||||
# flake8: noqa
|
||||
|
||||
"""
|
||||
InfluxDB OSS API Service.
|
||||
|
||||
The InfluxDB v2 API provides a programmatic interface for all interactions with InfluxDB. Access the InfluxDB API using the `/api/v2/` endpoint. # noqa: E501
|
||||
|
||||
OpenAPI spec version: 2.0.0
|
||||
Generated by: https://openapi-generator.tech
|
||||
"""
|
||||
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
# import apis into sdk package
|
||||
from influxdb_client.service.authorizations_service import AuthorizationsService
|
||||
from influxdb_client.service.backup_service import BackupService
|
||||
from influxdb_client.service.bucket_schemas_service import BucketSchemasService
|
||||
from influxdb_client.service.buckets_service import BucketsService
|
||||
from influxdb_client.service.cells_service import CellsService
|
||||
from influxdb_client.service.checks_service import ChecksService
|
||||
from influxdb_client.service.config_service import ConfigService
|
||||
from influxdb_client.service.dbr_ps_service import DBRPsService
|
||||
from influxdb_client.service.dashboards_service import DashboardsService
|
||||
from influxdb_client.service.delete_service import DeleteService
|
||||
from influxdb_client.service.health_service import HealthService
|
||||
from influxdb_client.service.invokable_scripts_service import InvokableScriptsService
|
||||
from influxdb_client.service.labels_service import LabelsService
|
||||
from influxdb_client.service.legacy_authorizations_service import LegacyAuthorizationsService
|
||||
from influxdb_client.service.metrics_service import MetricsService
|
||||
from influxdb_client.service.notification_endpoints_service import NotificationEndpointsService
|
||||
from influxdb_client.service.notification_rules_service import NotificationRulesService
|
||||
from influxdb_client.service.organizations_service import OrganizationsService
|
||||
from influxdb_client.service.ping_service import PingService
|
||||
from influxdb_client.service.query_service import QueryService
|
||||
from influxdb_client.service.ready_service import ReadyService
|
||||
from influxdb_client.service.remote_connections_service import RemoteConnectionsService
|
||||
from influxdb_client.service.replications_service import ReplicationsService
|
||||
from influxdb_client.service.resources_service import ResourcesService
|
||||
from influxdb_client.service.restore_service import RestoreService
|
||||
from influxdb_client.service.routes_service import RoutesService
|
||||
from influxdb_client.service.rules_service import RulesService
|
||||
from influxdb_client.service.scraper_targets_service import ScraperTargetsService
|
||||
from influxdb_client.service.secrets_service import SecretsService
|
||||
from influxdb_client.service.setup_service import SetupService
|
||||
from influxdb_client.service.signin_service import SigninService
|
||||
from influxdb_client.service.signout_service import SignoutService
|
||||
from influxdb_client.service.sources_service import SourcesService
|
||||
from influxdb_client.service.tasks_service import TasksService
|
||||
from influxdb_client.service.telegraf_plugins_service import TelegrafPluginsService
|
||||
from influxdb_client.service.telegrafs_service import TelegrafsService
|
||||
from influxdb_client.service.templates_service import TemplatesService
|
||||
from influxdb_client.service.users_service import UsersService
|
||||
from influxdb_client.service.variables_service import VariablesService
|
||||
from influxdb_client.service.views_service import ViewsService
|
||||
from influxdb_client.service.write_service import WriteService
|
||||
|
||||
from influxdb_client.configuration import Configuration
|
||||
# import models into sdk package
|
||||
from influxdb_client.domain.ast_response import ASTResponse
|
||||
from influxdb_client.domain.add_resource_member_request_body import AddResourceMemberRequestBody
|
||||
from influxdb_client.domain.analyze_query_response import AnalyzeQueryResponse
|
||||
from influxdb_client.domain.analyze_query_response_errors import AnalyzeQueryResponseErrors
|
||||
from influxdb_client.domain.array_expression import ArrayExpression
|
||||
from influxdb_client.domain.authorization import Authorization
|
||||
from influxdb_client.domain.authorization_post_request import AuthorizationPostRequest
|
||||
from influxdb_client.domain.authorization_update_request import AuthorizationUpdateRequest
|
||||
from influxdb_client.domain.authorizations import Authorizations
|
||||
from influxdb_client.domain.axes import Axes
|
||||
from influxdb_client.domain.axis import Axis
|
||||
from influxdb_client.domain.axis_scale import AxisScale
|
||||
from influxdb_client.domain.bad_statement import BadStatement
|
||||
from influxdb_client.domain.band_view_properties import BandViewProperties
|
||||
from influxdb_client.domain.binary_expression import BinaryExpression
|
||||
from influxdb_client.domain.block import Block
|
||||
from influxdb_client.domain.boolean_literal import BooleanLiteral
|
||||
from influxdb_client.domain.bucket import Bucket
|
||||
from influxdb_client.domain.bucket_links import BucketLinks
|
||||
from influxdb_client.domain.bucket_metadata_manifest import BucketMetadataManifest
|
||||
from influxdb_client.domain.bucket_retention_rules import BucketRetentionRules
|
||||
from influxdb_client.domain.bucket_shard_mapping import BucketShardMapping
|
||||
from influxdb_client.domain.buckets import Buckets
|
||||
from influxdb_client.domain.builder_aggregate_function_type import BuilderAggregateFunctionType
|
||||
from influxdb_client.domain.builder_config import BuilderConfig
|
||||
from influxdb_client.domain.builder_config_aggregate_window import BuilderConfigAggregateWindow
|
||||
from influxdb_client.domain.builder_functions_type import BuilderFunctionsType
|
||||
from influxdb_client.domain.builder_tags_type import BuilderTagsType
|
||||
from influxdb_client.domain.builtin_statement import BuiltinStatement
|
||||
from influxdb_client.domain.call_expression import CallExpression
|
||||
from influxdb_client.domain.cell import Cell
|
||||
from influxdb_client.domain.cell_links import CellLinks
|
||||
from influxdb_client.domain.cell_update import CellUpdate
|
||||
from influxdb_client.domain.cell_with_view_properties import CellWithViewProperties
|
||||
from influxdb_client.domain.check import Check
|
||||
from influxdb_client.domain.check_base import CheckBase
|
||||
from influxdb_client.domain.check_base_links import CheckBaseLinks
|
||||
from influxdb_client.domain.check_discriminator import CheckDiscriminator
|
||||
from influxdb_client.domain.check_patch import CheckPatch
|
||||
from influxdb_client.domain.check_status_level import CheckStatusLevel
|
||||
from influxdb_client.domain.check_view_properties import CheckViewProperties
|
||||
from influxdb_client.domain.checks import Checks
|
||||
from influxdb_client.domain.column_data_type import ColumnDataType
|
||||
from influxdb_client.domain.column_semantic_type import ColumnSemanticType
|
||||
from influxdb_client.domain.conditional_expression import ConditionalExpression
|
||||
from influxdb_client.domain.config import Config
|
||||
from influxdb_client.domain.constant_variable_properties import ConstantVariableProperties
|
||||
from influxdb_client.domain.create_cell import CreateCell
|
||||
from influxdb_client.domain.create_dashboard_request import CreateDashboardRequest
|
||||
from influxdb_client.domain.custom_check import CustomCheck
|
||||
from influxdb_client.domain.dbrp import DBRP
|
||||
from influxdb_client.domain.dbrp_create import DBRPCreate
|
||||
from influxdb_client.domain.dbrp_get import DBRPGet
|
||||
from influxdb_client.domain.dbrp_update import DBRPUpdate
|
||||
from influxdb_client.domain.dbr_ps import DBRPs
|
||||
from influxdb_client.domain.dashboard import Dashboard
|
||||
from influxdb_client.domain.dashboard_color import DashboardColor
|
||||
from influxdb_client.domain.dashboard_query import DashboardQuery
|
||||
from influxdb_client.domain.dashboard_with_view_properties import DashboardWithViewProperties
|
||||
from influxdb_client.domain.dashboards import Dashboards
|
||||
from influxdb_client.domain.date_time_literal import DateTimeLiteral
|
||||
from influxdb_client.domain.deadman_check import DeadmanCheck
|
||||
from influxdb_client.domain.decimal_places import DecimalPlaces
|
||||
from influxdb_client.domain.delete_predicate_request import DeletePredicateRequest
|
||||
from influxdb_client.domain.dialect import Dialect
|
||||
from influxdb_client.domain.dict_expression import DictExpression
|
||||
from influxdb_client.domain.dict_item import DictItem
|
||||
from influxdb_client.domain.duration import Duration
|
||||
from influxdb_client.domain.duration_literal import DurationLiteral
|
||||
from influxdb_client.domain.error import Error
|
||||
from influxdb_client.domain.expression import Expression
|
||||
from influxdb_client.domain.expression_statement import ExpressionStatement
|
||||
from influxdb_client.domain.field import Field
|
||||
from influxdb_client.domain.file import File
|
||||
from influxdb_client.domain.float_literal import FloatLiteral
|
||||
from influxdb_client.domain.flux_response import FluxResponse
|
||||
from influxdb_client.domain.flux_suggestion import FluxSuggestion
|
||||
from influxdb_client.domain.flux_suggestions import FluxSuggestions
|
||||
from influxdb_client.domain.function_expression import FunctionExpression
|
||||
from influxdb_client.domain.gauge_view_properties import GaugeViewProperties
|
||||
from influxdb_client.domain.greater_threshold import GreaterThreshold
|
||||
from influxdb_client.domain.http_notification_endpoint import HTTPNotificationEndpoint
|
||||
from influxdb_client.domain.http_notification_rule import HTTPNotificationRule
|
||||
from influxdb_client.domain.http_notification_rule_base import HTTPNotificationRuleBase
|
||||
from influxdb_client.domain.health_check import HealthCheck
|
||||
from influxdb_client.domain.heatmap_view_properties import HeatmapViewProperties
|
||||
from influxdb_client.domain.histogram_view_properties import HistogramViewProperties
|
||||
from influxdb_client.domain.identifier import Identifier
|
||||
from influxdb_client.domain.import_declaration import ImportDeclaration
|
||||
from influxdb_client.domain.index_expression import IndexExpression
|
||||
from influxdb_client.domain.integer_literal import IntegerLiteral
|
||||
from influxdb_client.domain.is_onboarding import IsOnboarding
|
||||
from influxdb_client.domain.label import Label
|
||||
from influxdb_client.domain.label_create_request import LabelCreateRequest
|
||||
from influxdb_client.domain.label_mapping import LabelMapping
|
||||
from influxdb_client.domain.label_response import LabelResponse
|
||||
from influxdb_client.domain.label_update import LabelUpdate
|
||||
from influxdb_client.domain.labels_response import LabelsResponse
|
||||
from influxdb_client.domain.language_request import LanguageRequest
|
||||
from influxdb_client.domain.legacy_authorization_post_request import LegacyAuthorizationPostRequest
|
||||
from influxdb_client.domain.lesser_threshold import LesserThreshold
|
||||
from influxdb_client.domain.line_plus_single_stat_properties import LinePlusSingleStatProperties
|
||||
from influxdb_client.domain.line_protocol_error import LineProtocolError
|
||||
from influxdb_client.domain.line_protocol_length_error import LineProtocolLengthError
|
||||
from influxdb_client.domain.links import Links
|
||||
from influxdb_client.domain.list_stacks_response import ListStacksResponse
|
||||
from influxdb_client.domain.log_event import LogEvent
|
||||
from influxdb_client.domain.logical_expression import LogicalExpression
|
||||
from influxdb_client.domain.logs import Logs
|
||||
from influxdb_client.domain.map_variable_properties import MapVariableProperties
|
||||
from influxdb_client.domain.markdown_view_properties import MarkdownViewProperties
|
||||
from influxdb_client.domain.measurement_schema import MeasurementSchema
|
||||
from influxdb_client.domain.measurement_schema_column import MeasurementSchemaColumn
|
||||
from influxdb_client.domain.measurement_schema_create_request import MeasurementSchemaCreateRequest
|
||||
from influxdb_client.domain.measurement_schema_list import MeasurementSchemaList
|
||||
from influxdb_client.domain.measurement_schema_update_request import MeasurementSchemaUpdateRequest
|
||||
from influxdb_client.domain.member_assignment import MemberAssignment
|
||||
from influxdb_client.domain.member_expression import MemberExpression
|
||||
from influxdb_client.domain.metadata_backup import MetadataBackup
|
||||
from influxdb_client.domain.model_property import ModelProperty
|
||||
from influxdb_client.domain.mosaic_view_properties import MosaicViewProperties
|
||||
from influxdb_client.domain.node import Node
|
||||
from influxdb_client.domain.notification_endpoint import NotificationEndpoint
|
||||
from influxdb_client.domain.notification_endpoint_base import NotificationEndpointBase
|
||||
from influxdb_client.domain.notification_endpoint_base_links import NotificationEndpointBaseLinks
|
||||
from influxdb_client.domain.notification_endpoint_discriminator import NotificationEndpointDiscriminator
|
||||
from influxdb_client.domain.notification_endpoint_type import NotificationEndpointType
|
||||
from influxdb_client.domain.notification_endpoint_update import NotificationEndpointUpdate
|
||||
from influxdb_client.domain.notification_endpoints import NotificationEndpoints
|
||||
from influxdb_client.domain.notification_rule import NotificationRule
|
||||
from influxdb_client.domain.notification_rule_base import NotificationRuleBase
|
||||
from influxdb_client.domain.notification_rule_base_links import NotificationRuleBaseLinks
|
||||
from influxdb_client.domain.notification_rule_discriminator import NotificationRuleDiscriminator
|
||||
from influxdb_client.domain.notification_rule_update import NotificationRuleUpdate
|
||||
from influxdb_client.domain.notification_rules import NotificationRules
|
||||
from influxdb_client.domain.object_expression import ObjectExpression
|
||||
from influxdb_client.domain.onboarding_request import OnboardingRequest
|
||||
from influxdb_client.domain.onboarding_response import OnboardingResponse
|
||||
from influxdb_client.domain.option_statement import OptionStatement
|
||||
from influxdb_client.domain.organization import Organization
|
||||
from influxdb_client.domain.organization_links import OrganizationLinks
|
||||
from influxdb_client.domain.organizations import Organizations
|
||||
from influxdb_client.domain.package import Package
|
||||
from influxdb_client.domain.package_clause import PackageClause
|
||||
from influxdb_client.domain.pager_duty_notification_endpoint import PagerDutyNotificationEndpoint
|
||||
from influxdb_client.domain.pager_duty_notification_rule import PagerDutyNotificationRule
|
||||
from influxdb_client.domain.pager_duty_notification_rule_base import PagerDutyNotificationRuleBase
|
||||
from influxdb_client.domain.paren_expression import ParenExpression
|
||||
from influxdb_client.domain.password_reset_body import PasswordResetBody
|
||||
from influxdb_client.domain.patch_bucket_request import PatchBucketRequest
|
||||
from influxdb_client.domain.patch_dashboard_request import PatchDashboardRequest
|
||||
from influxdb_client.domain.patch_organization_request import PatchOrganizationRequest
|
||||
from influxdb_client.domain.patch_retention_rule import PatchRetentionRule
|
||||
from influxdb_client.domain.patch_stack_request import PatchStackRequest
|
||||
from influxdb_client.domain.patch_stack_request_additional_resources import PatchStackRequestAdditionalResources
|
||||
from influxdb_client.domain.permission import Permission
|
||||
from influxdb_client.domain.permission_resource import PermissionResource
|
||||
from influxdb_client.domain.pipe_expression import PipeExpression
|
||||
from influxdb_client.domain.pipe_literal import PipeLiteral
|
||||
from influxdb_client.domain.post_bucket_request import PostBucketRequest
|
||||
from influxdb_client.domain.post_check import PostCheck
|
||||
from influxdb_client.domain.post_notification_endpoint import PostNotificationEndpoint
|
||||
from influxdb_client.domain.post_notification_rule import PostNotificationRule
|
||||
from influxdb_client.domain.post_organization_request import PostOrganizationRequest
|
||||
from influxdb_client.domain.post_restore_kv_response import PostRestoreKVResponse
|
||||
from influxdb_client.domain.post_stack_request import PostStackRequest
|
||||
from influxdb_client.domain.property_key import PropertyKey
|
||||
from influxdb_client.domain.query import Query
|
||||
from influxdb_client.domain.query_edit_mode import QueryEditMode
|
||||
from influxdb_client.domain.query_variable_properties import QueryVariableProperties
|
||||
from influxdb_client.domain.query_variable_properties_values import QueryVariablePropertiesValues
|
||||
from influxdb_client.domain.range_threshold import RangeThreshold
|
||||
from influxdb_client.domain.ready import Ready
|
||||
from influxdb_client.domain.regexp_literal import RegexpLiteral
|
||||
from influxdb_client.domain.remote_connection import RemoteConnection
|
||||
from influxdb_client.domain.remote_connection_creation_request import RemoteConnectionCreationRequest
|
||||
from influxdb_client.domain.remote_connection_update_request import RemoteConnectionUpdateRequest
|
||||
from influxdb_client.domain.remote_connections import RemoteConnections
|
||||
from influxdb_client.domain.renamable_field import RenamableField
|
||||
from influxdb_client.domain.replication import Replication
|
||||
from influxdb_client.domain.replication_creation_request import ReplicationCreationRequest
|
||||
from influxdb_client.domain.replication_update_request import ReplicationUpdateRequest
|
||||
from influxdb_client.domain.replications import Replications
|
||||
from influxdb_client.domain.resource_member import ResourceMember
|
||||
from influxdb_client.domain.resource_members import ResourceMembers
|
||||
from influxdb_client.domain.resource_members_links import ResourceMembersLinks
|
||||
from influxdb_client.domain.resource_owner import ResourceOwner
|
||||
from influxdb_client.domain.resource_owners import ResourceOwners
|
||||
from influxdb_client.domain.restored_bucket_mappings import RestoredBucketMappings
|
||||
from influxdb_client.domain.retention_policy_manifest import RetentionPolicyManifest
|
||||
from influxdb_client.domain.return_statement import ReturnStatement
|
||||
from influxdb_client.domain.routes import Routes
|
||||
from influxdb_client.domain.routes_external import RoutesExternal
|
||||
from influxdb_client.domain.routes_query import RoutesQuery
|
||||
from influxdb_client.domain.routes_system import RoutesSystem
|
||||
from influxdb_client.domain.rule_status_level import RuleStatusLevel
|
||||
from influxdb_client.domain.run import Run
|
||||
from influxdb_client.domain.run_links import RunLinks
|
||||
from influxdb_client.domain.run_manually import RunManually
|
||||
from influxdb_client.domain.runs import Runs
|
||||
from influxdb_client.domain.smtp_notification_rule import SMTPNotificationRule
|
||||
from influxdb_client.domain.smtp_notification_rule_base import SMTPNotificationRuleBase
|
||||
from influxdb_client.domain.scatter_view_properties import ScatterViewProperties
|
||||
from influxdb_client.domain.schema_type import SchemaType
|
||||
from influxdb_client.domain.scraper_target_request import ScraperTargetRequest
|
||||
from influxdb_client.domain.scraper_target_response import ScraperTargetResponse
|
||||
from influxdb_client.domain.scraper_target_responses import ScraperTargetResponses
|
||||
from influxdb_client.domain.script import Script
|
||||
from influxdb_client.domain.script_create_request import ScriptCreateRequest
|
||||
from influxdb_client.domain.script_invocation_params import ScriptInvocationParams
|
||||
from influxdb_client.domain.script_language import ScriptLanguage
|
||||
from influxdb_client.domain.script_update_request import ScriptUpdateRequest
|
||||
from influxdb_client.domain.scripts import Scripts
|
||||
from influxdb_client.domain.secret_keys import SecretKeys
|
||||
from influxdb_client.domain.secret_keys_response import SecretKeysResponse
|
||||
from influxdb_client.domain.shard_group_manifest import ShardGroupManifest
|
||||
from influxdb_client.domain.shard_manifest import ShardManifest
|
||||
from influxdb_client.domain.shard_owner import ShardOwner
|
||||
from influxdb_client.domain.simple_table_view_properties import SimpleTableViewProperties
|
||||
from influxdb_client.domain.single_stat_view_properties import SingleStatViewProperties
|
||||
from influxdb_client.domain.slack_notification_endpoint import SlackNotificationEndpoint
|
||||
from influxdb_client.domain.slack_notification_rule import SlackNotificationRule
|
||||
from influxdb_client.domain.slack_notification_rule_base import SlackNotificationRuleBase
|
||||
from influxdb_client.domain.source import Source
|
||||
from influxdb_client.domain.source_links import SourceLinks
|
||||
from influxdb_client.domain.sources import Sources
|
||||
from influxdb_client.domain.stack import Stack
|
||||
from influxdb_client.domain.stack_associations import StackAssociations
|
||||
from influxdb_client.domain.stack_events import StackEvents
|
||||
from influxdb_client.domain.stack_links import StackLinks
|
||||
from influxdb_client.domain.stack_resources import StackResources
|
||||
from influxdb_client.domain.statement import Statement
|
||||
from influxdb_client.domain.static_legend import StaticLegend
|
||||
from influxdb_client.domain.status_rule import StatusRule
|
||||
from influxdb_client.domain.string_literal import StringLiteral
|
||||
from influxdb_client.domain.subscription_manifest import SubscriptionManifest
|
||||
from influxdb_client.domain.table_view_properties import TableViewProperties
|
||||
from influxdb_client.domain.table_view_properties_table_options import TableViewPropertiesTableOptions
|
||||
from influxdb_client.domain.tag_rule import TagRule
|
||||
from influxdb_client.domain.task import Task
|
||||
from influxdb_client.domain.task_create_request import TaskCreateRequest
|
||||
from influxdb_client.domain.task_links import TaskLinks
|
||||
from influxdb_client.domain.task_status_type import TaskStatusType
|
||||
from influxdb_client.domain.task_update_request import TaskUpdateRequest
|
||||
from influxdb_client.domain.tasks import Tasks
|
||||
from influxdb_client.domain.telegraf import Telegraf
|
||||
from influxdb_client.domain.telegraf_plugin import TelegrafPlugin
|
||||
from influxdb_client.domain.telegraf_plugin_request import TelegrafPluginRequest
|
||||
from influxdb_client.domain.telegraf_plugin_request_plugins import TelegrafPluginRequestPlugins
|
||||
from influxdb_client.domain.telegraf_plugins import TelegrafPlugins
|
||||
from influxdb_client.domain.telegraf_request import TelegrafRequest
|
||||
from influxdb_client.domain.telegraf_request_metadata import TelegrafRequestMetadata
|
||||
from influxdb_client.domain.telegrafs import Telegrafs
|
||||
from influxdb_client.domain.telegram_notification_endpoint import TelegramNotificationEndpoint
|
||||
from influxdb_client.domain.telegram_notification_rule import TelegramNotificationRule
|
||||
from influxdb_client.domain.telegram_notification_rule_base import TelegramNotificationRuleBase
|
||||
from influxdb_client.domain.template_apply import TemplateApply
|
||||
from influxdb_client.domain.template_apply_remotes import TemplateApplyRemotes
|
||||
from influxdb_client.domain.template_apply_template import TemplateApplyTemplate
|
||||
from influxdb_client.domain.template_chart import TemplateChart
|
||||
from influxdb_client.domain.template_export_by_id import TemplateExportByID
|
||||
from influxdb_client.domain.template_export_by_id_org_ids import TemplateExportByIDOrgIDs
|
||||
from influxdb_client.domain.template_export_by_id_resource_filters import TemplateExportByIDResourceFilters
|
||||
from influxdb_client.domain.template_export_by_id_resources import TemplateExportByIDResources
|
||||
from influxdb_client.domain.template_kind import TemplateKind
|
||||
from influxdb_client.domain.template_summary import TemplateSummary
|
||||
from influxdb_client.domain.template_summary_diff import TemplateSummaryDiff
|
||||
from influxdb_client.domain.template_summary_diff_buckets import TemplateSummaryDiffBuckets
|
||||
from influxdb_client.domain.template_summary_diff_buckets_new_old import TemplateSummaryDiffBucketsNewOld
|
||||
from influxdb_client.domain.template_summary_diff_checks import TemplateSummaryDiffChecks
|
||||
from influxdb_client.domain.template_summary_diff_dashboards import TemplateSummaryDiffDashboards
|
||||
from influxdb_client.domain.template_summary_diff_dashboards_new_old import TemplateSummaryDiffDashboardsNewOld
|
||||
from influxdb_client.domain.template_summary_diff_label_mappings import TemplateSummaryDiffLabelMappings
|
||||
from influxdb_client.domain.template_summary_diff_labels import TemplateSummaryDiffLabels
|
||||
from influxdb_client.domain.template_summary_diff_labels_new_old import TemplateSummaryDiffLabelsNewOld
|
||||
from influxdb_client.domain.template_summary_diff_notification_endpoints import TemplateSummaryDiffNotificationEndpoints
|
||||
from influxdb_client.domain.template_summary_diff_notification_rules import TemplateSummaryDiffNotificationRules
|
||||
from influxdb_client.domain.template_summary_diff_notification_rules_new_old import TemplateSummaryDiffNotificationRulesNewOld
|
||||
from influxdb_client.domain.template_summary_diff_tasks import TemplateSummaryDiffTasks
|
||||
from influxdb_client.domain.template_summary_diff_tasks_new_old import TemplateSummaryDiffTasksNewOld
|
||||
from influxdb_client.domain.template_summary_diff_telegraf_configs import TemplateSummaryDiffTelegrafConfigs
|
||||
from influxdb_client.domain.template_summary_diff_variables import TemplateSummaryDiffVariables
|
||||
from influxdb_client.domain.template_summary_diff_variables_new_old import TemplateSummaryDiffVariablesNewOld
|
||||
from influxdb_client.domain.template_summary_errors import TemplateSummaryErrors
|
||||
from influxdb_client.domain.template_summary_label import TemplateSummaryLabel
|
||||
from influxdb_client.domain.template_summary_label_properties import TemplateSummaryLabelProperties
|
||||
from influxdb_client.domain.template_summary_summary import TemplateSummarySummary
|
||||
from influxdb_client.domain.template_summary_summary_buckets import TemplateSummarySummaryBuckets
|
||||
from influxdb_client.domain.template_summary_summary_dashboards import TemplateSummarySummaryDashboards
|
||||
from influxdb_client.domain.template_summary_summary_label_mappings import TemplateSummarySummaryLabelMappings
|
||||
from influxdb_client.domain.template_summary_summary_notification_rules import TemplateSummarySummaryNotificationRules
|
||||
from influxdb_client.domain.template_summary_summary_status_rules import TemplateSummarySummaryStatusRules
|
||||
from influxdb_client.domain.template_summary_summary_tag_rules import TemplateSummarySummaryTagRules
|
||||
from influxdb_client.domain.template_summary_summary_tasks import TemplateSummarySummaryTasks
|
||||
from influxdb_client.domain.template_summary_summary_variables import TemplateSummarySummaryVariables
|
||||
from influxdb_client.domain.test_statement import TestStatement
|
||||
from influxdb_client.domain.threshold import Threshold
|
||||
from influxdb_client.domain.threshold_base import ThresholdBase
|
||||
from influxdb_client.domain.threshold_check import ThresholdCheck
|
||||
from influxdb_client.domain.unary_expression import UnaryExpression
|
||||
from influxdb_client.domain.unsigned_integer_literal import UnsignedIntegerLiteral
|
||||
from influxdb_client.domain.user import User
|
||||
from influxdb_client.domain.user_response import UserResponse
|
||||
from influxdb_client.domain.user_response_links import UserResponseLinks
|
||||
from influxdb_client.domain.users import Users
|
||||
from influxdb_client.domain.variable import Variable
|
||||
from influxdb_client.domain.variable_assignment import VariableAssignment
|
||||
from influxdb_client.domain.variable_links import VariableLinks
|
||||
from influxdb_client.domain.variable_properties import VariableProperties
|
||||
from influxdb_client.domain.variables import Variables
|
||||
from influxdb_client.domain.view import View
|
||||
from influxdb_client.domain.view_links import ViewLinks
|
||||
from influxdb_client.domain.view_properties import ViewProperties
|
||||
from influxdb_client.domain.views import Views
|
||||
from influxdb_client.domain.write_precision import WritePrecision
|
||||
from influxdb_client.domain.xy_geom import XYGeom
|
||||
from influxdb_client.domain.xy_view_properties import XYViewProperties
|
||||
|
||||
from influxdb_client.client.authorizations_api import AuthorizationsApi
|
||||
from influxdb_client.client.bucket_api import BucketsApi
|
||||
from influxdb_client.client.delete_api import DeleteApi
|
||||
from influxdb_client.client.invokable_scripts_api import InvokableScriptsApi
|
||||
from influxdb_client.client.labels_api import LabelsApi
|
||||
from influxdb_client.client.organizations_api import OrganizationsApi
|
||||
from influxdb_client.client.query_api import QueryApi
|
||||
from influxdb_client.client.tasks_api import TasksApi
|
||||
from influxdb_client.client.users_api import UsersApi
|
||||
from influxdb_client.client.write_api import WriteApi, WriteOptions
|
||||
from influxdb_client.client.influxdb_client import InfluxDBClient
|
||||
from influxdb_client.client.logging_handler import InfluxLoggingHandler
|
||||
from influxdb_client.client.write.point import Point
|
||||
|
||||
from influxdb_client.version import VERSION
|
||||
|
||||
__version__ = VERSION
|
||||
@@ -0,0 +1 @@
|
||||
"""Asynchronous REST APIs."""
|
||||
@@ -0,0 +1,663 @@
|
||||
# coding: utf-8
|
||||
"""
|
||||
InfluxDB OSS API Service.
|
||||
|
||||
The InfluxDB v2 API provides a programmatic interface for all interactions with InfluxDB. Access the InfluxDB API using the `/api/v2/` endpoint. # noqa: E501
|
||||
|
||||
OpenAPI spec version: 2.0.0
|
||||
Generated by: https://openapi-generator.tech
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
import datetime
|
||||
import json
|
||||
import mimetypes
|
||||
import os
|
||||
import re
|
||||
import tempfile
|
||||
from multiprocessing.pool import ThreadPool
|
||||
from urllib.parse import quote
|
||||
|
||||
import influxdb_client.domain
|
||||
from influxdb_client import SigninService
|
||||
from influxdb_client import SignoutService
|
||||
from influxdb_client._async import rest
|
||||
from influxdb_client.configuration import Configuration
|
||||
from influxdb_client.rest import _requires_create_user_session, _requires_expire_user_session
|
||||
|
||||
|
||||
class ApiClientAsync(object):
|
||||
"""Generic API client for OpenAPI client library Build.
|
||||
|
||||
OpenAPI generic API client. This client handles the client-
|
||||
server communication, and is invariant across implementations. Specifics of
|
||||
the methods and models for each application are generated from the OpenAPI
|
||||
templates.
|
||||
|
||||
NOTE: This class is auto generated by OpenAPI Generator.
|
||||
Ref: https://openapi-generator.tech
|
||||
Do not edit the class manually.
|
||||
|
||||
:param configuration: .Configuration object for this client
|
||||
:param header_name: a header to pass when making calls to the API.
|
||||
:param header_value: a header value to pass when making calls to
|
||||
the API.
|
||||
:param cookie: a cookie to include in the header when making calls
|
||||
to the API
|
||||
:param pool_threads: The number of threads to use for async requests
|
||||
to the API. More threads means more concurrent API requests.
|
||||
"""
|
||||
|
||||
PRIMITIVE_TYPES = (float, bool, bytes, str, int)
|
||||
NATIVE_TYPES_MAPPING = {
|
||||
'int': int,
|
||||
'long': int,
|
||||
'float': float,
|
||||
'str': str,
|
||||
'bool': bool,
|
||||
'date': datetime.date,
|
||||
'datetime': datetime.datetime,
|
||||
'object': object,
|
||||
}
|
||||
_pool = None
|
||||
|
||||
def __init__(self, configuration=None, header_name=None, header_value=None,
|
||||
cookie=None, pool_threads=None, **kwargs):
|
||||
"""Initialize generic API client."""
|
||||
if configuration is None:
|
||||
configuration = Configuration()
|
||||
self.configuration = configuration
|
||||
self.pool_threads = pool_threads
|
||||
|
||||
self.rest_client = rest.RESTClientObjectAsync(configuration, **kwargs)
|
||||
self.default_headers = {}
|
||||
if header_name is not None:
|
||||
self.default_headers[header_name] = header_value
|
||||
self.cookie = cookie
|
||||
# Set default User-Agent.
|
||||
from influxdb_client import VERSION
|
||||
self.user_agent = f'influxdb-client-python/{VERSION}'
|
||||
|
||||
async def close(self):
|
||||
"""Dispose api client."""
|
||||
await self._signout()
|
||||
await self.rest_client.close()
|
||||
"""Dispose pools."""
|
||||
if self._pool:
|
||||
self._pool.close()
|
||||
self._pool.join()
|
||||
self._pool = None
|
||||
|
||||
@property
|
||||
def pool(self):
|
||||
"""Create thread pool on first request avoids instantiating unused threadpool for blocking clients."""
|
||||
if self._pool is None:
|
||||
self._pool = ThreadPool(self.pool_threads)
|
||||
return self._pool
|
||||
|
||||
@property
|
||||
def user_agent(self):
|
||||
"""User agent for this API client."""
|
||||
return self.default_headers['User-Agent']
|
||||
|
||||
@user_agent.setter
|
||||
def user_agent(self, value):
|
||||
"""Set User agent for this API client."""
|
||||
self.default_headers['User-Agent'] = value
|
||||
|
||||
def set_default_header(self, header_name, header_value):
|
||||
"""Set HTTP header for this API client."""
|
||||
self.default_headers[header_name] = header_value
|
||||
|
||||
async def __call_api(
|
||||
self, resource_path, method, path_params=None,
|
||||
query_params=None, header_params=None, body=None, post_params=None,
|
||||
files=None, response_type=None, auth_settings=None,
|
||||
_return_http_data_only=None, collection_formats=None,
|
||||
_preload_content=True, _request_timeout=None, urlopen_kw=None):
|
||||
|
||||
config = self.configuration
|
||||
await self._signin(resource_path=resource_path)
|
||||
|
||||
# header parameters
|
||||
header_params = header_params or {}
|
||||
config.update_request_header_params(resource_path, header_params)
|
||||
header_params.update(self.default_headers)
|
||||
if self.cookie:
|
||||
header_params['Cookie'] = self.cookie
|
||||
if header_params:
|
||||
header_params = self.sanitize_for_serialization(header_params)
|
||||
header_params = dict(self.parameters_to_tuples(header_params,
|
||||
collection_formats))
|
||||
|
||||
# path parameters
|
||||
if path_params:
|
||||
path_params = self.sanitize_for_serialization(path_params)
|
||||
path_params = self.parameters_to_tuples(path_params,
|
||||
collection_formats)
|
||||
for k, v in path_params:
|
||||
# specified safe chars, encode everything
|
||||
resource_path = resource_path.replace(
|
||||
'{%s}' % k,
|
||||
quote(str(v), safe=config.safe_chars_for_path_param)
|
||||
)
|
||||
|
||||
# query parameters
|
||||
if query_params:
|
||||
query_params = self.sanitize_for_serialization(query_params)
|
||||
query_params = self.parameters_to_tuples(query_params,
|
||||
collection_formats)
|
||||
|
||||
# post parameters
|
||||
if post_params or files:
|
||||
post_params = self.prepare_post_parameters(post_params, files)
|
||||
post_params = self.sanitize_for_serialization(post_params)
|
||||
post_params = self.parameters_to_tuples(post_params,
|
||||
collection_formats)
|
||||
|
||||
# auth setting
|
||||
self.update_params_for_auth(header_params, query_params, auth_settings)
|
||||
|
||||
# body
|
||||
if body:
|
||||
body = self.sanitize_for_serialization(body)
|
||||
body = config.update_request_body(resource_path, body)
|
||||
|
||||
# request url
|
||||
url = self.configuration.host + resource_path
|
||||
|
||||
urlopen_kw = urlopen_kw or {}
|
||||
|
||||
# perform request and return response
|
||||
response_data = await self.request(
|
||||
method, url, query_params=query_params, headers=header_params,
|
||||
post_params=post_params, body=body,
|
||||
_preload_content=_preload_content,
|
||||
_request_timeout=_request_timeout, **urlopen_kw)
|
||||
|
||||
self.last_response = response_data
|
||||
|
||||
return_data = response_data
|
||||
if _preload_content:
|
||||
# deserialize response data
|
||||
if response_type:
|
||||
return_data = self.deserialize(response_data, response_type)
|
||||
else:
|
||||
return_data = None
|
||||
|
||||
if _return_http_data_only is not False:
|
||||
return return_data
|
||||
else:
|
||||
return (return_data, response_data.status,
|
||||
response_data.getheaders())
|
||||
|
||||
def sanitize_for_serialization(self, obj):
|
||||
"""Build a JSON POST object.
|
||||
|
||||
If obj is None, return None.
|
||||
If obj is str, int, long, float, bool, return directly.
|
||||
If obj is datetime.datetime, datetime.date
|
||||
convert to string in iso8601 format.
|
||||
If obj is list, sanitize each element in the list.
|
||||
If obj is dict, return the dict.
|
||||
If obj is OpenAPI model, return the properties dict.
|
||||
|
||||
:param obj: The data to serialize.
|
||||
:return: The serialized form of data.
|
||||
"""
|
||||
if obj is None:
|
||||
return None
|
||||
elif isinstance(obj, self.PRIMITIVE_TYPES):
|
||||
return obj
|
||||
elif isinstance(obj, list):
|
||||
return [self.sanitize_for_serialization(sub_obj)
|
||||
for sub_obj in obj]
|
||||
elif isinstance(obj, tuple):
|
||||
return tuple(self.sanitize_for_serialization(sub_obj)
|
||||
for sub_obj in obj)
|
||||
elif isinstance(obj, (datetime.datetime, datetime.date)):
|
||||
return obj.isoformat()
|
||||
|
||||
if isinstance(obj, dict):
|
||||
obj_dict = obj
|
||||
else:
|
||||
# Convert model obj to dict except
|
||||
# attributes `openapi_types`, `attribute_map`
|
||||
# and attributes which value is not None.
|
||||
# Convert attribute name to json key in
|
||||
# model definition for request.
|
||||
obj_dict = {obj.attribute_map[attr]: getattr(obj, attr)
|
||||
for attr, _ in obj.openapi_types.items()
|
||||
if getattr(obj, attr) is not None}
|
||||
|
||||
return {key: self.sanitize_for_serialization(val)
|
||||
for key, val in obj_dict.items()}
|
||||
|
||||
def deserialize(self, response, response_type):
|
||||
"""Deserializes response into an object.
|
||||
|
||||
:param response: RESTResponse object to be deserialized.
|
||||
:param response_type: class literal for
|
||||
deserialized object, or string of class name.
|
||||
|
||||
:return: deserialized object.
|
||||
"""
|
||||
# handle file downloading
|
||||
# save response body into a tmp file and return the instance
|
||||
if response_type == "file":
|
||||
return self.__deserialize_file(response)
|
||||
|
||||
# fetch data from response object
|
||||
try:
|
||||
data = json.loads(response.data)
|
||||
except ValueError:
|
||||
data = response.data
|
||||
|
||||
return self.__deserialize(data, response_type)
|
||||
|
||||
def __deserialize(self, data, klass):
|
||||
"""Deserializes dict, list, str into an object.
|
||||
|
||||
:param data: dict, list or str.
|
||||
:param klass: class literal, or string of class name.
|
||||
|
||||
:return: object.
|
||||
"""
|
||||
if data is None:
|
||||
return None
|
||||
|
||||
if type(klass) == str:
|
||||
if klass.startswith('list['):
|
||||
sub_kls = re.match(r'list\[(.*)\]', klass).group(1)
|
||||
return [self.__deserialize(sub_data, sub_kls)
|
||||
for sub_data in data]
|
||||
|
||||
if klass.startswith('dict('):
|
||||
sub_kls = re.match(r'dict\(([^,]*), (.*)\)', klass).group(2)
|
||||
return {k: self.__deserialize(v, sub_kls)
|
||||
for k, v in data.items()}
|
||||
|
||||
# convert str to class
|
||||
if klass in self.NATIVE_TYPES_MAPPING:
|
||||
klass = self.NATIVE_TYPES_MAPPING[klass]
|
||||
else:
|
||||
klass = getattr(influxdb_client.domain, klass)
|
||||
|
||||
if klass in self.PRIMITIVE_TYPES:
|
||||
return self.__deserialize_primitive(data, klass)
|
||||
elif klass == object:
|
||||
return self.__deserialize_object(data)
|
||||
elif klass == datetime.date:
|
||||
return self.__deserialize_date(data)
|
||||
elif klass == datetime.datetime:
|
||||
return self.__deserialize_datatime(data)
|
||||
else:
|
||||
return self.__deserialize_model(data, klass)
|
||||
|
||||
def call_api(self, resource_path, method,
|
||||
path_params=None, query_params=None, header_params=None,
|
||||
body=None, post_params=None, files=None,
|
||||
response_type=None, auth_settings=None, async_req=None,
|
||||
_return_http_data_only=None, collection_formats=None,
|
||||
_preload_content=True, _request_timeout=None, urlopen_kw=None):
|
||||
"""Make the HTTP request (synchronous) and Return deserialized data.
|
||||
|
||||
To make an async_req request, set the async_req parameter.
|
||||
|
||||
:param resource_path: Path to method endpoint.
|
||||
:param method: Method to call.
|
||||
:param path_params: Path parameters in the url.
|
||||
:param query_params: Query parameters in the url.
|
||||
:param header_params: Header parameters to be
|
||||
placed in the request header.
|
||||
:param body: Request body.
|
||||
:param post_params dict: Request post form parameters,
|
||||
for `application/x-www-form-urlencoded`, `multipart/form-data`.
|
||||
:param auth_settings list: Auth Settings names for the request.
|
||||
:param response: Response data type.
|
||||
:param files dict: key -> filename, value -> filepath,
|
||||
for `multipart/form-data`.
|
||||
:param async_req bool: execute request asynchronously
|
||||
:param _return_http_data_only: response data without head status code
|
||||
and headers
|
||||
:param collection_formats: dict of collection formats for path, query,
|
||||
header, and post parameters.
|
||||
:param _preload_content: if False, the urllib3.HTTPResponse object will
|
||||
be returned without reading/decoding response
|
||||
data. Default is True.
|
||||
:param _request_timeout: timeout setting for this request. If one
|
||||
number provided, it will be total request
|
||||
timeout. It can also be a pair (tuple) of
|
||||
(connection, read) timeouts.
|
||||
:param urlopen_kw: Additional parameters are passed to
|
||||
:meth:`urllib3.request.RequestMethods.request`
|
||||
:return:
|
||||
If async_req parameter is True,
|
||||
the request will be called asynchronously.
|
||||
The method will return the request thread.
|
||||
If parameter async_req is False or missing,
|
||||
then the method will return the response directly.
|
||||
"""
|
||||
if not async_req:
|
||||
return self.__call_api(resource_path, method,
|
||||
path_params, query_params, header_params,
|
||||
body, post_params, files,
|
||||
response_type, auth_settings,
|
||||
_return_http_data_only, collection_formats,
|
||||
_preload_content, _request_timeout, urlopen_kw)
|
||||
else:
|
||||
thread = self.pool.apply_async(self.__call_api, (resource_path,
|
||||
method, path_params, query_params,
|
||||
header_params, body,
|
||||
post_params, files,
|
||||
response_type, auth_settings,
|
||||
_return_http_data_only,
|
||||
collection_formats,
|
||||
_preload_content, _request_timeout, urlopen_kw))
|
||||
return thread
|
||||
|
||||
def request(self, method, url, query_params=None, headers=None,
|
||||
post_params=None, body=None, _preload_content=True,
|
||||
_request_timeout=None, **urlopen_kw):
|
||||
"""Make the HTTP request using RESTClient."""
|
||||
if method == "GET":
|
||||
return self.rest_client.GET(url,
|
||||
query_params=query_params,
|
||||
_preload_content=_preload_content,
|
||||
_request_timeout=_request_timeout,
|
||||
headers=headers,
|
||||
**urlopen_kw)
|
||||
elif method == "HEAD":
|
||||
return self.rest_client.HEAD(url,
|
||||
query_params=query_params,
|
||||
_preload_content=_preload_content,
|
||||
_request_timeout=_request_timeout,
|
||||
headers=headers,
|
||||
**urlopen_kw)
|
||||
elif method == "OPTIONS":
|
||||
return self.rest_client.OPTIONS(url,
|
||||
query_params=query_params,
|
||||
headers=headers,
|
||||
post_params=post_params,
|
||||
_preload_content=_preload_content,
|
||||
_request_timeout=_request_timeout,
|
||||
body=body,
|
||||
**urlopen_kw)
|
||||
elif method == "POST":
|
||||
return self.rest_client.POST(url,
|
||||
query_params=query_params,
|
||||
headers=headers,
|
||||
post_params=post_params,
|
||||
_preload_content=_preload_content,
|
||||
_request_timeout=_request_timeout,
|
||||
body=body,
|
||||
**urlopen_kw)
|
||||
elif method == "PUT":
|
||||
return self.rest_client.PUT(url,
|
||||
query_params=query_params,
|
||||
headers=headers,
|
||||
post_params=post_params,
|
||||
_preload_content=_preload_content,
|
||||
_request_timeout=_request_timeout,
|
||||
body=body,
|
||||
**urlopen_kw)
|
||||
elif method == "PATCH":
|
||||
return self.rest_client.PATCH(url,
|
||||
query_params=query_params,
|
||||
headers=headers,
|
||||
post_params=post_params,
|
||||
_preload_content=_preload_content,
|
||||
_request_timeout=_request_timeout,
|
||||
body=body,
|
||||
**urlopen_kw)
|
||||
elif method == "DELETE":
|
||||
return self.rest_client.DELETE(url,
|
||||
query_params=query_params,
|
||||
headers=headers,
|
||||
_preload_content=_preload_content,
|
||||
_request_timeout=_request_timeout,
|
||||
body=body,
|
||||
**urlopen_kw)
|
||||
else:
|
||||
raise ValueError(
|
||||
"http method must be `GET`, `HEAD`, `OPTIONS`,"
|
||||
" `POST`, `PATCH`, `PUT` or `DELETE`."
|
||||
)
|
||||
|
||||
def parameters_to_tuples(self, params, collection_formats):
|
||||
"""Get parameters as list of tuples, formatting collections.
|
||||
|
||||
:param params: Parameters as dict or list of two-tuples
|
||||
:param dict collection_formats: Parameter collection formats
|
||||
:return: Parameters as list of tuples, collections formatted
|
||||
"""
|
||||
new_params = []
|
||||
if collection_formats is None:
|
||||
collection_formats = {}
|
||||
for k, v in params.items() if isinstance(params, dict) else params: # noqa: E501
|
||||
if k in collection_formats:
|
||||
collection_format = collection_formats[k]
|
||||
if collection_format == 'multi':
|
||||
new_params.extend((k, value) for value in v)
|
||||
else:
|
||||
if collection_format == 'ssv':
|
||||
delimiter = ' '
|
||||
elif collection_format == 'tsv':
|
||||
delimiter = '\t'
|
||||
elif collection_format == 'pipes':
|
||||
delimiter = '|'
|
||||
else: # csv is the default
|
||||
delimiter = ','
|
||||
new_params.append(
|
||||
(k, delimiter.join(str(value) for value in v)))
|
||||
else:
|
||||
new_params.append((k, v))
|
||||
return new_params
|
||||
|
||||
def prepare_post_parameters(self, post_params=None, files=None):
|
||||
"""Build form parameters.
|
||||
|
||||
:param post_params: Normal form parameters.
|
||||
:param files: File parameters.
|
||||
:return: Form parameters with files.
|
||||
"""
|
||||
params = []
|
||||
|
||||
if post_params:
|
||||
params = post_params
|
||||
|
||||
if files:
|
||||
for k, v in files.items():
|
||||
if not v:
|
||||
continue
|
||||
file_names = v if type(v) is list else [v]
|
||||
for n in file_names:
|
||||
with open(n, 'rb') as f:
|
||||
filename = os.path.basename(f.name)
|
||||
filedata = f.read()
|
||||
mimetype = (mimetypes.guess_type(filename)[0] or
|
||||
'application/octet-stream')
|
||||
params.append(
|
||||
tuple([k, tuple([filename, filedata, mimetype])]))
|
||||
|
||||
return params
|
||||
|
||||
def select_header_accept(self, accepts):
|
||||
"""Return `Accept` based on an array of accepts provided.
|
||||
|
||||
:param accepts: List of headers.
|
||||
:return: Accept (e.g. application/json).
|
||||
"""
|
||||
if not accepts:
|
||||
return
|
||||
|
||||
accepts = [x.lower() for x in accepts]
|
||||
|
||||
if 'application/json' in accepts:
|
||||
return 'application/json'
|
||||
else:
|
||||
return ', '.join(accepts)
|
||||
|
||||
def select_header_content_type(self, content_types):
|
||||
"""Return `Content-Type` based on an array of content_types provided.
|
||||
|
||||
:param content_types: List of content-types.
|
||||
:return: Content-Type (e.g. application/json).
|
||||
"""
|
||||
if not content_types:
|
||||
return 'application/json'
|
||||
|
||||
content_types = [x.lower() for x in content_types]
|
||||
|
||||
if 'application/json' in content_types or '*/*' in content_types:
|
||||
return 'application/json'
|
||||
else:
|
||||
return content_types[0]
|
||||
|
||||
def update_params_for_auth(self, headers, querys, auth_settings):
|
||||
"""Update header and query params based on authentication setting.
|
||||
|
||||
:param headers: Header parameters dict to be updated.
|
||||
:param querys: Query parameters tuple list to be updated.
|
||||
:param auth_settings: Authentication setting identifiers list.
|
||||
"""
|
||||
if not auth_settings:
|
||||
return
|
||||
|
||||
for auth in auth_settings:
|
||||
auth_setting = self.configuration.auth_settings().get(auth)
|
||||
if auth_setting:
|
||||
if not auth_setting['value']:
|
||||
continue
|
||||
elif auth_setting['in'] == 'header':
|
||||
headers[auth_setting['key']] = auth_setting['value']
|
||||
elif auth_setting['in'] == 'query':
|
||||
querys.append((auth_setting['key'], auth_setting['value']))
|
||||
else:
|
||||
raise ValueError(
|
||||
'Authentication token must be in `query` or `header`'
|
||||
)
|
||||
|
||||
def __deserialize_file(self, response):
|
||||
"""Deserializes body to file.
|
||||
|
||||
Saves response body into a file in a temporary folder,
|
||||
using the filename from the `Content-Disposition` header if provided.
|
||||
|
||||
:param response: RESTResponse.
|
||||
:return: file path.
|
||||
"""
|
||||
fd, path = tempfile.mkstemp(dir=self.configuration.temp_folder_path)
|
||||
os.close(fd)
|
||||
os.remove(path)
|
||||
|
||||
content_disposition = response.getheader("Content-Disposition")
|
||||
if content_disposition:
|
||||
filename = re.search(r'filename=[\'"]?([^\'"\s]+)[\'"]?',
|
||||
content_disposition).group(1)
|
||||
path = os.path.join(os.path.dirname(path), filename)
|
||||
|
||||
with open(path, "wb") as f:
|
||||
f.write(response.data)
|
||||
|
||||
return path
|
||||
|
||||
def __deserialize_primitive(self, data, klass):
|
||||
"""Deserializes string to primitive type.
|
||||
|
||||
:param data: str.
|
||||
:param klass: class literal.
|
||||
|
||||
:return: int, long, float, str, bool.
|
||||
"""
|
||||
try:
|
||||
return klass(data)
|
||||
except UnicodeEncodeError:
|
||||
return str(data)
|
||||
except TypeError:
|
||||
return data
|
||||
|
||||
def __deserialize_object(self, value):
|
||||
"""Return an original value.
|
||||
|
||||
:return: object.
|
||||
"""
|
||||
return value
|
||||
|
||||
def __deserialize_date(self, string):
|
||||
"""Deserializes string to date.
|
||||
|
||||
:param string: str.
|
||||
:return: date.
|
||||
"""
|
||||
try:
|
||||
from dateutil.parser import parse
|
||||
return parse(string).date()
|
||||
except ImportError:
|
||||
return string
|
||||
except ValueError:
|
||||
raise rest.ApiException(
|
||||
status=0,
|
||||
reason="Failed to parse `{0}` as date object".format(string)
|
||||
)
|
||||
|
||||
def __deserialize_datatime(self, string):
|
||||
"""Deserializes string to datetime.
|
||||
|
||||
The string should be in iso8601 datetime format.
|
||||
|
||||
:param string: str.
|
||||
:return: datetime.
|
||||
"""
|
||||
try:
|
||||
from dateutil.parser import parse
|
||||
return parse(string)
|
||||
except ImportError:
|
||||
return string
|
||||
except ValueError:
|
||||
raise rest.ApiException(
|
||||
status=0,
|
||||
reason=(
|
||||
"Failed to parse `{0}` as datetime object"
|
||||
.format(string)
|
||||
)
|
||||
)
|
||||
|
||||
def __deserialize_model(self, data, klass):
|
||||
"""Deserializes list or dict to model.
|
||||
|
||||
:param data: dict, list.
|
||||
:param klass: class literal.
|
||||
:return: model object.
|
||||
"""
|
||||
if not klass.openapi_types and not hasattr(klass,
|
||||
'get_real_child_model'):
|
||||
return data
|
||||
|
||||
kwargs = {}
|
||||
if klass.openapi_types is not None:
|
||||
for attr, attr_type in klass.openapi_types.items():
|
||||
if (data is not None and
|
||||
klass.attribute_map[attr] in data and
|
||||
isinstance(data, (list, dict))):
|
||||
value = data[klass.attribute_map[attr]]
|
||||
kwargs[attr] = self.__deserialize(value, attr_type)
|
||||
|
||||
instance = klass(**kwargs)
|
||||
|
||||
if hasattr(instance, 'get_real_child_model'):
|
||||
klass_name = instance.get_real_child_model(data)
|
||||
if klass_name:
|
||||
instance = self.__deserialize(data, klass_name)
|
||||
return instance
|
||||
|
||||
async def _signin(self, resource_path: str):
|
||||
if _requires_create_user_session(self.configuration, self.cookie, resource_path):
|
||||
http_info = await SigninService(self).post_signin_async(_return_http_data_only=False)
|
||||
self.cookie = http_info[2]['set-cookie']
|
||||
|
||||
async def _signout(self):
|
||||
if _requires_expire_user_session(self.configuration, self.cookie):
|
||||
await SignoutService(self).post_signout_async()
|
||||
self.cookie = None
|
||||
@@ -0,0 +1,309 @@
|
||||
"""
|
||||
InfluxDB OSS API Service.
|
||||
|
||||
The InfluxDB v2 API provides a programmatic interface for all interactions with InfluxDB. Access the InfluxDB API using the `/api/v2/` endpoint. # noqa: E501
|
||||
|
||||
OpenAPI spec version: 2.0.0
|
||||
Generated by: https://openapi-generator.tech
|
||||
"""
|
||||
|
||||
|
||||
import io
|
||||
import json
|
||||
import re
|
||||
import ssl
|
||||
from urllib.parse import urlencode
|
||||
|
||||
import aiohttp
|
||||
|
||||
from influxdb_client.rest import ApiException
|
||||
from influxdb_client.rest import _BaseRESTClient
|
||||
from influxdb_client.rest import _UTF_8_encoding
|
||||
|
||||
|
||||
async def _on_request_start(session, trace_config_ctx, params):
|
||||
_BaseRESTClient.log_request(params.method, params.url)
|
||||
_BaseRESTClient.log_headers(params.headers, '>>>')
|
||||
|
||||
|
||||
async def _on_request_chunk_sent(session, context, params):
|
||||
if params.chunk:
|
||||
_BaseRESTClient.log_body(params.chunk, '>>>')
|
||||
|
||||
|
||||
async def _on_request_end(session, trace_config_ctx, params):
|
||||
_BaseRESTClient.log_response(params.response.status)
|
||||
_BaseRESTClient.log_headers(params.headers, '<<<')
|
||||
|
||||
response_content = params.response.content
|
||||
data = bytearray()
|
||||
while True:
|
||||
chunk = await response_content.read(100)
|
||||
if not chunk:
|
||||
break
|
||||
data += chunk
|
||||
if data:
|
||||
_BaseRESTClient.log_body(data.decode(_UTF_8_encoding), '<<<')
|
||||
response_content.unread_data(data=data)
|
||||
|
||||
|
||||
class RESTResponseAsync(io.IOBase):
|
||||
"""NOTE: This class is auto generated by OpenAPI Generator.
|
||||
|
||||
Ref: https://openapi-generator.tech
|
||||
Do not edit the class manually.
|
||||
"""
|
||||
|
||||
def __init__(self, resp, data):
|
||||
"""Initialize with HTTP response."""
|
||||
self.aiohttp_response = resp
|
||||
self.status = resp.status
|
||||
self.reason = resp.reason
|
||||
self.data = data
|
||||
|
||||
def getheaders(self):
|
||||
"""Return a CIMultiDictProxy of the response headers."""
|
||||
return self.aiohttp_response.headers
|
||||
|
||||
def getheader(self, name, default=None):
|
||||
"""Return a given response header."""
|
||||
return self.aiohttp_response.headers.get(name, default)
|
||||
|
||||
|
||||
class RESTClientObjectAsync(object):
|
||||
"""NOTE: This class is auto generated by OpenAPI Generator.
|
||||
|
||||
Ref: https://openapi-generator.tech
|
||||
Do not edit the class manually.
|
||||
"""
|
||||
|
||||
def __init__(self, configuration, pools_size=4, maxsize=None, **kwargs):
|
||||
"""Initialize REST client."""
|
||||
# maxsize is number of requests to host that are allowed in parallel
|
||||
if maxsize is None:
|
||||
maxsize = configuration.connection_pool_maxsize
|
||||
|
||||
if configuration.ssl_context is None:
|
||||
ssl_context = ssl.create_default_context(cafile=configuration.ssl_ca_cert)
|
||||
if configuration.cert_file:
|
||||
ssl_context.load_cert_chain(
|
||||
certfile=configuration.cert_file, keyfile=configuration.cert_key_file,
|
||||
password=configuration.cert_key_password
|
||||
)
|
||||
|
||||
if not configuration.verify_ssl:
|
||||
ssl_context.check_hostname = False
|
||||
ssl_context.verify_mode = ssl.CERT_NONE
|
||||
else:
|
||||
ssl_context = configuration.ssl_context
|
||||
|
||||
connector = aiohttp.TCPConnector(
|
||||
limit=maxsize,
|
||||
ssl=ssl_context
|
||||
)
|
||||
|
||||
self.proxy = configuration.proxy
|
||||
self.proxy_headers = configuration.proxy_headers
|
||||
self.allow_redirects = kwargs.get('allow_redirects', True)
|
||||
self.max_redirects = kwargs.get('max_redirects', 10)
|
||||
|
||||
# configure tracing
|
||||
trace_config = aiohttp.TraceConfig()
|
||||
trace_config.on_request_start.append(_on_request_start)
|
||||
trace_config.on_request_chunk_sent.append(_on_request_chunk_sent)
|
||||
trace_config.on_request_end.append(_on_request_end)
|
||||
|
||||
# timeout
|
||||
if isinstance(configuration.timeout, (int, float,)): # noqa: E501,F821
|
||||
timeout = aiohttp.ClientTimeout(total=configuration.timeout / 1_000)
|
||||
elif isinstance(configuration.timeout, aiohttp.ClientTimeout):
|
||||
timeout = configuration.timeout
|
||||
else:
|
||||
timeout = aiohttp.client.DEFAULT_TIMEOUT
|
||||
|
||||
# https pool manager
|
||||
_client_session_type = kwargs.get('client_session_type', aiohttp.ClientSession)
|
||||
_client_session_kwargs = kwargs.get('client_session_kwargs', {})
|
||||
self.pool_manager = _client_session_type(
|
||||
connector=connector,
|
||||
timeout=timeout,
|
||||
trace_configs=[trace_config] if configuration.debug else None,
|
||||
**_client_session_kwargs
|
||||
)
|
||||
|
||||
async def close(self):
|
||||
"""Dispose connection pool manager."""
|
||||
await self.pool_manager.close()
|
||||
|
||||
async def request(self, method, url, query_params=None, headers=None,
|
||||
body=None, post_params=None, _preload_content=True,
|
||||
_request_timeout=None):
|
||||
"""Execute request.
|
||||
|
||||
:param method: http request method
|
||||
:param url: http request url
|
||||
:param query_params: query parameters in the url
|
||||
:param headers: http request headers
|
||||
:param body: request json body, for `application/json`
|
||||
:param post_params: request post parameters,
|
||||
`application/x-www-form-urlencoded`
|
||||
and `multipart/form-data`
|
||||
:param _preload_content: this is a non-applicable field for
|
||||
the AiohttpClient.
|
||||
:param _request_timeout: timeout setting for this request. If one
|
||||
number provided, it will be total request
|
||||
timeout. It can also be a pair (tuple) of
|
||||
(connection, read) timeouts.
|
||||
"""
|
||||
method = method.upper()
|
||||
assert method in ['GET', 'HEAD', 'DELETE', 'POST', 'PUT',
|
||||
'PATCH', 'OPTIONS']
|
||||
|
||||
if post_params and body:
|
||||
raise ValueError(
|
||||
"body parameter cannot be used with post_params parameter."
|
||||
)
|
||||
|
||||
post_params = post_params or {}
|
||||
headers = headers or {}
|
||||
|
||||
if 'Content-Type' not in headers:
|
||||
headers['Content-Type'] = 'application/json'
|
||||
|
||||
args = {
|
||||
"method": method,
|
||||
"url": url,
|
||||
"headers": headers,
|
||||
"allow_redirects": self.allow_redirects,
|
||||
"max_redirects": self.max_redirects
|
||||
}
|
||||
|
||||
if self.proxy:
|
||||
args["proxy"] = self.proxy
|
||||
if self.proxy_headers:
|
||||
args["proxy_headers"] = self.proxy_headers
|
||||
|
||||
if query_params:
|
||||
args["url"] += '?' + urlencode(query_params)
|
||||
|
||||
# For `POST`, `PUT`, `PATCH`, `OPTIONS`, `DELETE`
|
||||
if method in ['POST', 'PUT', 'PATCH', 'OPTIONS', 'DELETE']:
|
||||
if re.search('json', headers['Content-Type'], re.IGNORECASE):
|
||||
if body is not None:
|
||||
body = json.dumps(body)
|
||||
args["data"] = body
|
||||
elif headers['Content-Type'] == 'application/x-www-form-urlencoded': # noqa: E501
|
||||
args["data"] = aiohttp.FormData(post_params)
|
||||
elif headers['Content-Type'] == 'multipart/form-data':
|
||||
# must del headers['Content-Type'], or the correct
|
||||
# Content-Type which generated by aiohttp
|
||||
del headers['Content-Type']
|
||||
data = aiohttp.FormData()
|
||||
for param in post_params:
|
||||
k, v = param
|
||||
if isinstance(v, tuple) and len(v) == 3:
|
||||
data.add_field(k,
|
||||
value=v[1],
|
||||
filename=v[0],
|
||||
content_type=v[2])
|
||||
else:
|
||||
data.add_field(k, v)
|
||||
args["data"] = data
|
||||
|
||||
# Pass a `bytes` parameter directly in the body to support
|
||||
# other content types than Json when `body` argument is provided
|
||||
# in serialized form
|
||||
elif isinstance(body, bytes):
|
||||
args["data"] = body
|
||||
else:
|
||||
# Cannot generate the request from given parameters
|
||||
msg = """Cannot prepare a request message for provided
|
||||
arguments. Please check that your arguments match
|
||||
declared content type."""
|
||||
raise ApiException(status=0, reason=msg)
|
||||
|
||||
r = await self.pool_manager.request(**args)
|
||||
if _preload_content:
|
||||
|
||||
data = await r.read()
|
||||
r = RESTResponseAsync(r, data)
|
||||
|
||||
if not 200 <= r.status <= 299:
|
||||
raise ApiException(http_resp=r)
|
||||
|
||||
return r
|
||||
|
||||
async def GET(self, url, headers=None, query_params=None,
|
||||
_preload_content=True, _request_timeout=None):
|
||||
"""Perform GET HTTP request."""
|
||||
return (await self.request("GET", url,
|
||||
headers=headers,
|
||||
_preload_content=_preload_content,
|
||||
_request_timeout=_request_timeout,
|
||||
query_params=query_params))
|
||||
|
||||
async def HEAD(self, url, headers=None, query_params=None,
|
||||
_preload_content=True, _request_timeout=None):
|
||||
"""Perform HEAD HTTP request."""
|
||||
return (await self.request("HEAD", url,
|
||||
headers=headers,
|
||||
_preload_content=_preload_content,
|
||||
_request_timeout=_request_timeout,
|
||||
query_params=query_params))
|
||||
|
||||
async def OPTIONS(self, url, headers=None, query_params=None,
|
||||
post_params=None, body=None, _preload_content=True,
|
||||
_request_timeout=None):
|
||||
"""Perform OPTIONS HTTP request."""
|
||||
return (await self.request("OPTIONS", url,
|
||||
headers=headers,
|
||||
query_params=query_params,
|
||||
post_params=post_params,
|
||||
_preload_content=_preload_content,
|
||||
_request_timeout=_request_timeout,
|
||||
body=body))
|
||||
|
||||
async def DELETE(self, url, headers=None, query_params=None, body=None,
|
||||
_preload_content=True, _request_timeout=None):
|
||||
"""Perform DELETE HTTP request."""
|
||||
return (await self.request("DELETE", url,
|
||||
headers=headers,
|
||||
query_params=query_params,
|
||||
_preload_content=_preload_content,
|
||||
_request_timeout=_request_timeout,
|
||||
body=body))
|
||||
|
||||
async def POST(self, url, headers=None, query_params=None,
|
||||
post_params=None, body=None, _preload_content=True,
|
||||
_request_timeout=None):
|
||||
"""Perform POST HTTP request."""
|
||||
return (await self.request("POST", url,
|
||||
headers=headers,
|
||||
query_params=query_params,
|
||||
post_params=post_params,
|
||||
_preload_content=_preload_content,
|
||||
_request_timeout=_request_timeout,
|
||||
body=body))
|
||||
|
||||
async def PUT(self, url, headers=None, query_params=None, post_params=None,
|
||||
body=None, _preload_content=True, _request_timeout=None):
|
||||
"""Perform PUT HTTP request."""
|
||||
return (await self.request("PUT", url,
|
||||
headers=headers,
|
||||
query_params=query_params,
|
||||
post_params=post_params,
|
||||
_preload_content=_preload_content,
|
||||
_request_timeout=_request_timeout,
|
||||
body=body))
|
||||
|
||||
async def PATCH(self, url, headers=None, query_params=None,
|
||||
post_params=None, body=None, _preload_content=True,
|
||||
_request_timeout=None):
|
||||
"""Perform PATCH HTTP request."""
|
||||
return (await self.request("PATCH", url,
|
||||
headers=headers,
|
||||
query_params=query_params,
|
||||
post_params=post_params,
|
||||
_preload_content=_preload_content,
|
||||
_request_timeout=_request_timeout,
|
||||
body=body))
|
||||
@@ -0,0 +1 @@
|
||||
"""Synchronous REST APIs."""
|
||||
@@ -0,0 +1,663 @@
|
||||
# coding: utf-8
|
||||
"""
|
||||
InfluxDB OSS API Service.
|
||||
|
||||
The InfluxDB v2 API provides a programmatic interface for all interactions with InfluxDB. Access the InfluxDB API using the `/api/v2/` endpoint. # noqa: E501
|
||||
|
||||
OpenAPI spec version: 2.0.0
|
||||
Generated by: https://openapi-generator.tech
|
||||
"""
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
import datetime
|
||||
import json
|
||||
import mimetypes
|
||||
import os
|
||||
import re
|
||||
import tempfile
|
||||
from multiprocessing.pool import ThreadPool
|
||||
from urllib.parse import quote
|
||||
|
||||
import influxdb_client.domain
|
||||
from influxdb_client import SigninService
|
||||
from influxdb_client import SignoutService
|
||||
from influxdb_client._sync import rest
|
||||
from influxdb_client.configuration import Configuration
|
||||
from influxdb_client.rest import _requires_create_user_session, _requires_expire_user_session
|
||||
|
||||
|
||||
class ApiClient(object):
|
||||
"""Generic API client for OpenAPI client library Build.
|
||||
|
||||
OpenAPI generic API client. This client handles the client-
|
||||
server communication, and is invariant across implementations. Specifics of
|
||||
the methods and models for each application are generated from the OpenAPI
|
||||
templates.
|
||||
|
||||
NOTE: This class is auto generated by OpenAPI Generator.
|
||||
Ref: https://openapi-generator.tech
|
||||
Do not edit the class manually.
|
||||
|
||||
:param configuration: .Configuration object for this client
|
||||
:param header_name: a header to pass when making calls to the API.
|
||||
:param header_value: a header value to pass when making calls to
|
||||
the API.
|
||||
:param cookie: a cookie to include in the header when making calls
|
||||
to the API
|
||||
:param pool_threads: The number of threads to use for async requests
|
||||
to the API. More threads means more concurrent API requests.
|
||||
"""
|
||||
|
||||
PRIMITIVE_TYPES = (float, bool, bytes, str, int)
|
||||
NATIVE_TYPES_MAPPING = {
|
||||
'int': int,
|
||||
'long': int,
|
||||
'float': float,
|
||||
'str': str,
|
||||
'bool': bool,
|
||||
'date': datetime.date,
|
||||
'datetime': datetime.datetime,
|
||||
'object': object,
|
||||
}
|
||||
_pool = None
|
||||
|
||||
def __init__(self, configuration=None, header_name=None, header_value=None,
|
||||
cookie=None, pool_threads=None, retries=False):
|
||||
"""Initialize generic API client."""
|
||||
if configuration is None:
|
||||
configuration = Configuration()
|
||||
self.configuration = configuration
|
||||
self.pool_threads = pool_threads
|
||||
|
||||
self.rest_client = rest.RESTClientObject(configuration, retries=retries)
|
||||
self.default_headers = {}
|
||||
if header_name is not None:
|
||||
self.default_headers[header_name] = header_value
|
||||
self.cookie = cookie
|
||||
# Set default User-Agent.
|
||||
from influxdb_client import VERSION
|
||||
self.user_agent = f'influxdb-client-python/{VERSION}'
|
||||
|
||||
def __del__(self):
|
||||
"""Dispose pools."""
|
||||
self._signout()
|
||||
if self._pool:
|
||||
self._pool.close()
|
||||
self._pool.join()
|
||||
self._pool = None
|
||||
if self.rest_client and self.rest_client.pool_manager and hasattr(self.rest_client.pool_manager, 'clear'):
|
||||
self.rest_client.pool_manager.clear()
|
||||
|
||||
@property
|
||||
def pool(self):
|
||||
"""Create thread pool on first request avoids instantiating unused threadpool for blocking clients."""
|
||||
if self._pool is None:
|
||||
self._pool = ThreadPool(self.pool_threads)
|
||||
return self._pool
|
||||
|
||||
@property
|
||||
def user_agent(self):
|
||||
"""User agent for this API client."""
|
||||
return self.default_headers['User-Agent']
|
||||
|
||||
@user_agent.setter
|
||||
def user_agent(self, value):
|
||||
"""Set User agent for this API client."""
|
||||
self.default_headers['User-Agent'] = value
|
||||
|
||||
def set_default_header(self, header_name, header_value):
|
||||
"""Set HTTP header for this API client."""
|
||||
self.default_headers[header_name] = header_value
|
||||
|
||||
def __call_api(
|
||||
self, resource_path, method, path_params=None,
|
||||
query_params=None, header_params=None, body=None, post_params=None,
|
||||
files=None, response_type=None, auth_settings=None,
|
||||
_return_http_data_only=None, collection_formats=None,
|
||||
_preload_content=True, _request_timeout=None, urlopen_kw=None):
|
||||
|
||||
config = self.configuration
|
||||
self._signin(resource_path=resource_path)
|
||||
|
||||
# header parameters
|
||||
header_params = header_params or {}
|
||||
config.update_request_header_params(resource_path, header_params)
|
||||
header_params.update(self.default_headers)
|
||||
if self.cookie:
|
||||
header_params['Cookie'] = self.cookie
|
||||
if header_params:
|
||||
header_params = self.sanitize_for_serialization(header_params)
|
||||
header_params = dict(self.parameters_to_tuples(header_params,
|
||||
collection_formats))
|
||||
|
||||
# path parameters
|
||||
if path_params:
|
||||
path_params = self.sanitize_for_serialization(path_params)
|
||||
path_params = self.parameters_to_tuples(path_params,
|
||||
collection_formats)
|
||||
for k, v in path_params:
|
||||
# specified safe chars, encode everything
|
||||
resource_path = resource_path.replace(
|
||||
'{%s}' % k,
|
||||
quote(str(v), safe=config.safe_chars_for_path_param)
|
||||
)
|
||||
|
||||
# query parameters
|
||||
if query_params:
|
||||
query_params = self.sanitize_for_serialization(query_params)
|
||||
query_params = self.parameters_to_tuples(query_params,
|
||||
collection_formats)
|
||||
|
||||
# post parameters
|
||||
if post_params or files:
|
||||
post_params = self.prepare_post_parameters(post_params, files)
|
||||
post_params = self.sanitize_for_serialization(post_params)
|
||||
post_params = self.parameters_to_tuples(post_params,
|
||||
collection_formats)
|
||||
|
||||
# auth setting
|
||||
self.update_params_for_auth(header_params, query_params, auth_settings)
|
||||
|
||||
# body
|
||||
if body:
|
||||
body = self.sanitize_for_serialization(body)
|
||||
body = config.update_request_body(resource_path, body)
|
||||
|
||||
# request url
|
||||
url = self.configuration.host + resource_path
|
||||
|
||||
urlopen_kw = urlopen_kw or {}
|
||||
|
||||
# perform request and return response
|
||||
response_data = self.request(
|
||||
method, url, query_params=query_params, headers=header_params,
|
||||
post_params=post_params, body=body,
|
||||
_preload_content=_preload_content,
|
||||
_request_timeout=_request_timeout, **urlopen_kw)
|
||||
|
||||
self.last_response = response_data
|
||||
|
||||
return_data = response_data
|
||||
if _preload_content:
|
||||
# deserialize response data
|
||||
if response_type:
|
||||
return_data = self.deserialize(response_data, response_type)
|
||||
else:
|
||||
return_data = None
|
||||
|
||||
if _return_http_data_only:
|
||||
return (return_data)
|
||||
else:
|
||||
return (return_data, response_data.status,
|
||||
response_data.getheaders())
|
||||
|
||||
def sanitize_for_serialization(self, obj):
|
||||
"""Build a JSON POST object.
|
||||
|
||||
If obj is None, return None.
|
||||
If obj is str, int, long, float, bool, return directly.
|
||||
If obj is datetime.datetime, datetime.date
|
||||
convert to string in iso8601 format.
|
||||
If obj is list, sanitize each element in the list.
|
||||
If obj is dict, return the dict.
|
||||
If obj is OpenAPI model, return the properties dict.
|
||||
|
||||
:param obj: The data to serialize.
|
||||
:return: The serialized form of data.
|
||||
"""
|
||||
if obj is None:
|
||||
return None
|
||||
elif isinstance(obj, self.PRIMITIVE_TYPES):
|
||||
return obj
|
||||
elif isinstance(obj, list):
|
||||
return [self.sanitize_for_serialization(sub_obj)
|
||||
for sub_obj in obj]
|
||||
elif isinstance(obj, tuple):
|
||||
return tuple(self.sanitize_for_serialization(sub_obj)
|
||||
for sub_obj in obj)
|
||||
elif isinstance(obj, (datetime.datetime, datetime.date)):
|
||||
return obj.isoformat()
|
||||
|
||||
if isinstance(obj, dict):
|
||||
obj_dict = obj
|
||||
else:
|
||||
# Convert model obj to dict except
|
||||
# attributes `openapi_types`, `attribute_map`
|
||||
# and attributes which value is not None.
|
||||
# Convert attribute name to json key in
|
||||
# model definition for request.
|
||||
obj_dict = {obj.attribute_map[attr]: getattr(obj, attr)
|
||||
for attr, _ in obj.openapi_types.items()
|
||||
if getattr(obj, attr) is not None}
|
||||
|
||||
return {key: self.sanitize_for_serialization(val)
|
||||
for key, val in obj_dict.items()}
|
||||
|
||||
def deserialize(self, response, response_type):
|
||||
"""Deserializes response into an object.
|
||||
|
||||
:param response: RESTResponse object to be deserialized.
|
||||
:param response_type: class literal for
|
||||
deserialized object, or string of class name.
|
||||
|
||||
:return: deserialized object.
|
||||
"""
|
||||
# handle file downloading
|
||||
# save response body into a tmp file and return the instance
|
||||
if response_type == "file":
|
||||
return self.__deserialize_file(response)
|
||||
|
||||
# fetch data from response object
|
||||
try:
|
||||
data = json.loads(response.data)
|
||||
except ValueError:
|
||||
data = response.data
|
||||
|
||||
return self.__deserialize(data, response_type)
|
||||
|
||||
def __deserialize(self, data, klass):
|
||||
"""Deserializes dict, list, str into an object.
|
||||
|
||||
:param data: dict, list or str.
|
||||
:param klass: class literal, or string of class name.
|
||||
|
||||
:return: object.
|
||||
"""
|
||||
if data is None:
|
||||
return None
|
||||
|
||||
if type(klass) == str:
|
||||
if klass.startswith('list['):
|
||||
sub_kls = re.match(r'list\[(.*)\]', klass).group(1)
|
||||
return [self.__deserialize(sub_data, sub_kls)
|
||||
for sub_data in data]
|
||||
|
||||
if klass.startswith('dict('):
|
||||
sub_kls = re.match(r'dict\(([^,]*), (.*)\)', klass).group(2)
|
||||
return {k: self.__deserialize(v, sub_kls)
|
||||
for k, v in data.items()}
|
||||
|
||||
# convert str to class
|
||||
if klass in self.NATIVE_TYPES_MAPPING:
|
||||
klass = self.NATIVE_TYPES_MAPPING[klass]
|
||||
else:
|
||||
klass = getattr(influxdb_client.domain, klass)
|
||||
|
||||
if klass in self.PRIMITIVE_TYPES:
|
||||
return self.__deserialize_primitive(data, klass)
|
||||
elif klass == object:
|
||||
return self.__deserialize_object(data)
|
||||
elif klass == datetime.date:
|
||||
return self.__deserialize_date(data)
|
||||
elif klass == datetime.datetime:
|
||||
return self.__deserialize_datatime(data)
|
||||
else:
|
||||
return self.__deserialize_model(data, klass)
|
||||
|
||||
def call_api(self, resource_path, method,
|
||||
path_params=None, query_params=None, header_params=None,
|
||||
body=None, post_params=None, files=None,
|
||||
response_type=None, auth_settings=None, async_req=None,
|
||||
_return_http_data_only=None, collection_formats=None,
|
||||
_preload_content=True, _request_timeout=None, urlopen_kw=None):
|
||||
"""Make the HTTP request (synchronous) and Return deserialized data.
|
||||
|
||||
To make an async_req request, set the async_req parameter.
|
||||
|
||||
:param resource_path: Path to method endpoint.
|
||||
:param method: Method to call.
|
||||
:param path_params: Path parameters in the url.
|
||||
:param query_params: Query parameters in the url.
|
||||
:param header_params: Header parameters to be
|
||||
placed in the request header.
|
||||
:param body: Request body.
|
||||
:param post_params dict: Request post form parameters,
|
||||
for `application/x-www-form-urlencoded`, `multipart/form-data`.
|
||||
:param auth_settings list: Auth Settings names for the request.
|
||||
:param response: Response data type.
|
||||
:param files dict: key -> filename, value -> filepath,
|
||||
for `multipart/form-data`.
|
||||
:param async_req bool: execute request asynchronously
|
||||
:param _return_http_data_only: response data without head status code
|
||||
and headers
|
||||
:param collection_formats: dict of collection formats for path, query,
|
||||
header, and post parameters.
|
||||
:param _preload_content: if False, the urllib3.HTTPResponse object will
|
||||
be returned without reading/decoding response
|
||||
data. Default is True.
|
||||
:param _request_timeout: timeout setting for this request. If one
|
||||
number provided, it will be total request
|
||||
timeout. It can also be a pair (tuple) of
|
||||
(connection, read) timeouts.
|
||||
:param urlopen_kw: Additional parameters are passed to
|
||||
:meth:`urllib3.request.RequestMethods.request`
|
||||
:return:
|
||||
If async_req parameter is True,
|
||||
the request will be called asynchronously.
|
||||
The method will return the request thread.
|
||||
If parameter async_req is False or missing,
|
||||
then the method will return the response directly.
|
||||
"""
|
||||
if not async_req:
|
||||
return self.__call_api(resource_path, method,
|
||||
path_params, query_params, header_params,
|
||||
body, post_params, files,
|
||||
response_type, auth_settings,
|
||||
_return_http_data_only, collection_formats,
|
||||
_preload_content, _request_timeout, urlopen_kw)
|
||||
else:
|
||||
thread = self.pool.apply_async(self.__call_api, (resource_path,
|
||||
method, path_params, query_params,
|
||||
header_params, body,
|
||||
post_params, files,
|
||||
response_type, auth_settings,
|
||||
_return_http_data_only,
|
||||
collection_formats,
|
||||
_preload_content, _request_timeout, urlopen_kw))
|
||||
return thread
|
||||
|
||||
def request(self, method, url, query_params=None, headers=None,
|
||||
post_params=None, body=None, _preload_content=True,
|
||||
_request_timeout=None, **urlopen_kw):
|
||||
"""Make the HTTP request using RESTClient."""
|
||||
if method == "GET":
|
||||
return self.rest_client.GET(url,
|
||||
query_params=query_params,
|
||||
_preload_content=_preload_content,
|
||||
_request_timeout=_request_timeout,
|
||||
headers=headers,
|
||||
**urlopen_kw)
|
||||
elif method == "HEAD":
|
||||
return self.rest_client.HEAD(url,
|
||||
query_params=query_params,
|
||||
_preload_content=_preload_content,
|
||||
_request_timeout=_request_timeout,
|
||||
headers=headers,
|
||||
**urlopen_kw)
|
||||
elif method == "OPTIONS":
|
||||
return self.rest_client.OPTIONS(url,
|
||||
query_params=query_params,
|
||||
headers=headers,
|
||||
post_params=post_params,
|
||||
_preload_content=_preload_content,
|
||||
_request_timeout=_request_timeout,
|
||||
body=body,
|
||||
**urlopen_kw)
|
||||
elif method == "POST":
|
||||
return self.rest_client.POST(url,
|
||||
query_params=query_params,
|
||||
headers=headers,
|
||||
post_params=post_params,
|
||||
_preload_content=_preload_content,
|
||||
_request_timeout=_request_timeout,
|
||||
body=body,
|
||||
**urlopen_kw)
|
||||
elif method == "PUT":
|
||||
return self.rest_client.PUT(url,
|
||||
query_params=query_params,
|
||||
headers=headers,
|
||||
post_params=post_params,
|
||||
_preload_content=_preload_content,
|
||||
_request_timeout=_request_timeout,
|
||||
body=body,
|
||||
**urlopen_kw)
|
||||
elif method == "PATCH":
|
||||
return self.rest_client.PATCH(url,
|
||||
query_params=query_params,
|
||||
headers=headers,
|
||||
post_params=post_params,
|
||||
_preload_content=_preload_content,
|
||||
_request_timeout=_request_timeout,
|
||||
body=body,
|
||||
**urlopen_kw)
|
||||
elif method == "DELETE":
|
||||
return self.rest_client.DELETE(url,
|
||||
query_params=query_params,
|
||||
headers=headers,
|
||||
_preload_content=_preload_content,
|
||||
_request_timeout=_request_timeout,
|
||||
body=body,
|
||||
**urlopen_kw)
|
||||
else:
|
||||
raise ValueError(
|
||||
"http method must be `GET`, `HEAD`, `OPTIONS`,"
|
||||
" `POST`, `PATCH`, `PUT` or `DELETE`."
|
||||
)
|
||||
|
||||
def parameters_to_tuples(self, params, collection_formats):
|
||||
"""Get parameters as list of tuples, formatting collections.
|
||||
|
||||
:param params: Parameters as dict or list of two-tuples
|
||||
:param dict collection_formats: Parameter collection formats
|
||||
:return: Parameters as list of tuples, collections formatted
|
||||
"""
|
||||
new_params = []
|
||||
if collection_formats is None:
|
||||
collection_formats = {}
|
||||
for k, v in params.items() if isinstance(params, dict) else params: # noqa: E501
|
||||
if k in collection_formats:
|
||||
collection_format = collection_formats[k]
|
||||
if collection_format == 'multi':
|
||||
new_params.extend((k, value) for value in v)
|
||||
else:
|
||||
if collection_format == 'ssv':
|
||||
delimiter = ' '
|
||||
elif collection_format == 'tsv':
|
||||
delimiter = '\t'
|
||||
elif collection_format == 'pipes':
|
||||
delimiter = '|'
|
||||
else: # csv is the default
|
||||
delimiter = ','
|
||||
new_params.append(
|
||||
(k, delimiter.join(str(value) for value in v)))
|
||||
else:
|
||||
new_params.append((k, v))
|
||||
return new_params
|
||||
|
||||
def prepare_post_parameters(self, post_params=None, files=None):
|
||||
"""Build form parameters.
|
||||
|
||||
:param post_params: Normal form parameters.
|
||||
:param files: File parameters.
|
||||
:return: Form parameters with files.
|
||||
"""
|
||||
params = []
|
||||
|
||||
if post_params:
|
||||
params = post_params
|
||||
|
||||
if files:
|
||||
for k, v in files.items():
|
||||
if not v:
|
||||
continue
|
||||
file_names = v if type(v) is list else [v]
|
||||
for n in file_names:
|
||||
with open(n, 'rb') as f:
|
||||
filename = os.path.basename(f.name)
|
||||
filedata = f.read()
|
||||
mimetype = (mimetypes.guess_type(filename)[0] or
|
||||
'application/octet-stream')
|
||||
params.append(
|
||||
tuple([k, tuple([filename, filedata, mimetype])]))
|
||||
|
||||
return params
|
||||
|
||||
def select_header_accept(self, accepts):
|
||||
"""Return `Accept` based on an array of accepts provided.
|
||||
|
||||
:param accepts: List of headers.
|
||||
:return: Accept (e.g. application/json).
|
||||
"""
|
||||
if not accepts:
|
||||
return
|
||||
|
||||
accepts = [x.lower() for x in accepts]
|
||||
|
||||
if 'application/json' in accepts:
|
||||
return 'application/json'
|
||||
else:
|
||||
return ', '.join(accepts)
|
||||
|
||||
def select_header_content_type(self, content_types):
|
||||
"""Return `Content-Type` based on an array of content_types provided.
|
||||
|
||||
:param content_types: List of content-types.
|
||||
:return: Content-Type (e.g. application/json).
|
||||
"""
|
||||
if not content_types:
|
||||
return 'application/json'
|
||||
|
||||
content_types = [x.lower() for x in content_types]
|
||||
|
||||
if 'application/json' in content_types or '*/*' in content_types:
|
||||
return 'application/json'
|
||||
else:
|
||||
return content_types[0]
|
||||
|
||||
def update_params_for_auth(self, headers, querys, auth_settings):
|
||||
"""Update header and query params based on authentication setting.
|
||||
|
||||
:param headers: Header parameters dict to be updated.
|
||||
:param querys: Query parameters tuple list to be updated.
|
||||
:param auth_settings: Authentication setting identifiers list.
|
||||
"""
|
||||
if not auth_settings:
|
||||
return
|
||||
|
||||
for auth in auth_settings:
|
||||
auth_setting = self.configuration.auth_settings().get(auth)
|
||||
if auth_setting:
|
||||
if not auth_setting['value']:
|
||||
continue
|
||||
elif auth_setting['in'] == 'header':
|
||||
headers[auth_setting['key']] = auth_setting['value']
|
||||
elif auth_setting['in'] == 'query':
|
||||
querys.append((auth_setting['key'], auth_setting['value']))
|
||||
else:
|
||||
raise ValueError(
|
||||
'Authentication token must be in `query` or `header`'
|
||||
)
|
||||
|
||||
def __deserialize_file(self, response):
|
||||
"""Deserializes body to file.
|
||||
|
||||
Saves response body into a file in a temporary folder,
|
||||
using the filename from the `Content-Disposition` header if provided.
|
||||
|
||||
:param response: RESTResponse.
|
||||
:return: file path.
|
||||
"""
|
||||
fd, path = tempfile.mkstemp(dir=self.configuration.temp_folder_path)
|
||||
os.close(fd)
|
||||
os.remove(path)
|
||||
|
||||
content_disposition = response.getheader("Content-Disposition")
|
||||
if content_disposition:
|
||||
filename = re.search(r'filename=[\'"]?([^\'"\s]+)[\'"]?',
|
||||
content_disposition).group(1)
|
||||
path = os.path.join(os.path.dirname(path), filename)
|
||||
|
||||
with open(path, "wb") as f:
|
||||
f.write(response.data)
|
||||
|
||||
return path
|
||||
|
||||
def __deserialize_primitive(self, data, klass):
|
||||
"""Deserializes string to primitive type.
|
||||
|
||||
:param data: str.
|
||||
:param klass: class literal.
|
||||
|
||||
:return: int, long, float, str, bool.
|
||||
"""
|
||||
try:
|
||||
return klass(data)
|
||||
except UnicodeEncodeError:
|
||||
return str(data)
|
||||
except TypeError:
|
||||
return data
|
||||
|
||||
def __deserialize_object(self, value):
|
||||
"""Return an original value.
|
||||
|
||||
:return: object.
|
||||
"""
|
||||
return value
|
||||
|
||||
def __deserialize_date(self, string):
|
||||
"""Deserializes string to date.
|
||||
|
||||
:param string: str.
|
||||
:return: date.
|
||||
"""
|
||||
try:
|
||||
from dateutil.parser import parse
|
||||
return parse(string).date()
|
||||
except ImportError:
|
||||
return string
|
||||
except ValueError:
|
||||
raise rest.ApiException(
|
||||
status=0,
|
||||
reason="Failed to parse `{0}` as date object".format(string)
|
||||
)
|
||||
|
||||
def __deserialize_datatime(self, string):
|
||||
"""Deserializes string to datetime.
|
||||
|
||||
The string should be in iso8601 datetime format.
|
||||
|
||||
:param string: str.
|
||||
:return: datetime.
|
||||
"""
|
||||
try:
|
||||
from dateutil.parser import parse
|
||||
return parse(string)
|
||||
except ImportError:
|
||||
return string
|
||||
except ValueError:
|
||||
raise rest.ApiException(
|
||||
status=0,
|
||||
reason=(
|
||||
"Failed to parse `{0}` as datetime object"
|
||||
.format(string)
|
||||
)
|
||||
)
|
||||
|
||||
def __deserialize_model(self, data, klass):
|
||||
"""Deserializes list or dict to model.
|
||||
|
||||
:param data: dict, list.
|
||||
:param klass: class literal.
|
||||
:return: model object.
|
||||
"""
|
||||
if not klass.openapi_types and not hasattr(klass,
|
||||
'get_real_child_model'):
|
||||
return data
|
||||
|
||||
kwargs = {}
|
||||
if klass.openapi_types is not None:
|
||||
for attr, attr_type in klass.openapi_types.items():
|
||||
if (data is not None and
|
||||
klass.attribute_map[attr] in data and
|
||||
isinstance(data, (list, dict))):
|
||||
value = data[klass.attribute_map[attr]]
|
||||
kwargs[attr] = self.__deserialize(value, attr_type)
|
||||
|
||||
instance = klass(**kwargs)
|
||||
|
||||
if hasattr(instance, 'get_real_child_model'):
|
||||
klass_name = instance.get_real_child_model(data)
|
||||
if klass_name:
|
||||
instance = self.__deserialize(data, klass_name)
|
||||
return instance
|
||||
|
||||
def _signin(self, resource_path: str):
|
||||
if _requires_create_user_session(self.configuration, self.cookie, resource_path):
|
||||
http_info = SigninService(self).post_signin_with_http_info()
|
||||
self.cookie = http_info[2]['set-cookie']
|
||||
|
||||
def _signout(self):
|
||||
if _requires_expire_user_session(self.configuration, self.cookie):
|
||||
SignoutService(self).post_signout()
|
||||
self.cookie = None
|
||||
@@ -0,0 +1,355 @@
|
||||
# coding: utf-8
|
||||
|
||||
"""
|
||||
InfluxDB OSS API Service.
|
||||
|
||||
The InfluxDB v2 API provides a programmatic interface for all interactions with InfluxDB. Access the InfluxDB API using the `/api/v2/` endpoint. # noqa: E501
|
||||
|
||||
OpenAPI spec version: 2.0.0
|
||||
Generated by: https://openapi-generator.tech
|
||||
"""
|
||||
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
import io
|
||||
import json
|
||||
import re
|
||||
import ssl
|
||||
from urllib.parse import urlencode
|
||||
|
||||
from influxdb_client.rest import ApiException
|
||||
from influxdb_client.rest import _BaseRESTClient
|
||||
|
||||
try:
|
||||
import urllib3
|
||||
except ImportError:
|
||||
raise ImportError('OpenAPI Python client requires urllib3.')
|
||||
|
||||
|
||||
class RESTResponse(io.IOBase):
|
||||
"""NOTE: This class is auto generated by OpenAPI Generator.
|
||||
|
||||
Ref: https://openapi-generator.tech
|
||||
Do not edit the class manually.
|
||||
"""
|
||||
|
||||
def __init__(self, resp):
|
||||
"""Initialize with HTTP response."""
|
||||
self.urllib3_response = resp
|
||||
self.status = resp.status
|
||||
self.reason = resp.reason
|
||||
self.data = resp.data
|
||||
|
||||
def getheaders(self):
|
||||
"""Return a dictionary of the response headers."""
|
||||
return self.urllib3_response.headers
|
||||
|
||||
def getheader(self, name, default=None):
|
||||
"""Return a given response header."""
|
||||
return self.urllib3_response.headers.get(name, default)
|
||||
|
||||
|
||||
class RESTClientObject(object):
|
||||
"""NOTE: This class is auto generated by OpenAPI Generator.
|
||||
|
||||
Ref: https://openapi-generator.tech
|
||||
Do not edit the class manually.
|
||||
"""
|
||||
|
||||
def __init__(self, configuration, pools_size=4, maxsize=None, retries=False):
|
||||
"""Initialize REST client."""
|
||||
# urllib3.PoolManager will pass all kw parameters to connectionpool
|
||||
# https://github.com/shazow/urllib3/blob/f9409436f83aeb79fbaf090181cd81b784f1b8ce/urllib3/poolmanager.py#L75 # noqa: E501
|
||||
# https://github.com/shazow/urllib3/blob/f9409436f83aeb79fbaf090181cd81b784f1b8ce/urllib3/connectionpool.py#L680 # noqa: E501
|
||||
# maxsize is the number of requests to host that are allowed in parallel # noqa: E501
|
||||
# Custom SSL certificates and client certificates: http://urllib3.readthedocs.io/en/latest/advanced-usage.html # noqa: E501
|
||||
|
||||
self.configuration = configuration
|
||||
self.pools_size = pools_size
|
||||
self.maxsize = maxsize
|
||||
self.retries = retries
|
||||
|
||||
# cert_reqs
|
||||
if configuration.verify_ssl:
|
||||
cert_reqs = ssl.CERT_REQUIRED
|
||||
else:
|
||||
cert_reqs = ssl.CERT_NONE
|
||||
|
||||
# ca_certs
|
||||
if configuration.ssl_ca_cert:
|
||||
ca_certs = configuration.ssl_ca_cert
|
||||
else:
|
||||
ca_certs = None
|
||||
|
||||
addition_pool_args = {}
|
||||
if configuration.assert_hostname is not None:
|
||||
addition_pool_args['assert_hostname'] = configuration.assert_hostname # noqa: E501
|
||||
addition_pool_args['retries'] = self.retries
|
||||
|
||||
if maxsize is None:
|
||||
if configuration.connection_pool_maxsize is not None:
|
||||
maxsize = configuration.connection_pool_maxsize
|
||||
else:
|
||||
maxsize = 4
|
||||
|
||||
# https pool manager
|
||||
if configuration.proxy:
|
||||
self.pool_manager = urllib3.ProxyManager(
|
||||
num_pools=pools_size,
|
||||
maxsize=maxsize,
|
||||
cert_reqs=cert_reqs,
|
||||
ca_certs=ca_certs,
|
||||
cert_file=configuration.cert_file,
|
||||
key_file=configuration.cert_key_file,
|
||||
key_password=configuration.cert_key_password,
|
||||
proxy_url=configuration.proxy,
|
||||
proxy_headers=configuration.proxy_headers,
|
||||
ssl_context=configuration.ssl_context,
|
||||
**addition_pool_args
|
||||
)
|
||||
else:
|
||||
self.pool_manager = urllib3.PoolManager(
|
||||
num_pools=pools_size,
|
||||
maxsize=maxsize,
|
||||
cert_reqs=cert_reqs,
|
||||
ca_certs=ca_certs,
|
||||
cert_file=configuration.cert_file,
|
||||
key_file=configuration.cert_key_file,
|
||||
key_password=configuration.cert_key_password,
|
||||
ssl_context=configuration.ssl_context,
|
||||
**addition_pool_args
|
||||
)
|
||||
|
||||
def request(self, method, url, query_params=None, headers=None,
|
||||
body=None, post_params=None, _preload_content=True,
|
||||
_request_timeout=None, **urlopen_kw):
|
||||
"""Perform requests.
|
||||
|
||||
:param method: http request method
|
||||
:param url: http request url
|
||||
:param query_params: query parameters in the url
|
||||
:param headers: http request headers
|
||||
:param body: request json body, for `application/json`
|
||||
:param post_params: request post parameters,
|
||||
`application/x-www-form-urlencoded`
|
||||
and `multipart/form-data`
|
||||
:param _preload_content: if False, the urllib3.HTTPResponse object will
|
||||
be returned without reading/decoding response
|
||||
data. Default is True.
|
||||
:param _request_timeout: timeout setting for this request. If one
|
||||
number provided, it will be total request
|
||||
timeout. It can also be a pair (tuple) of
|
||||
(connection, read) timeouts.
|
||||
:param urlopen_kw: Additional parameters are passed to
|
||||
:meth:`urllib3.request.RequestMethods.request`
|
||||
"""
|
||||
method = method.upper()
|
||||
assert method in ['GET', 'HEAD', 'DELETE', 'POST', 'PUT',
|
||||
'PATCH', 'OPTIONS']
|
||||
|
||||
if post_params and body:
|
||||
raise ValueError(
|
||||
"body parameter cannot be used with post_params parameter."
|
||||
)
|
||||
|
||||
post_params = post_params or {}
|
||||
headers = headers or {}
|
||||
|
||||
timeout = None
|
||||
_configured_timeout = _request_timeout or self.configuration.timeout
|
||||
if _configured_timeout:
|
||||
if isinstance(_configured_timeout, (int, float, )): # noqa: E501,F821
|
||||
timeout = urllib3.Timeout(total=_configured_timeout / 1_000)
|
||||
elif (isinstance(_configured_timeout, tuple) and
|
||||
len(_configured_timeout) == 2):
|
||||
timeout = urllib3.Timeout(
|
||||
connect=_configured_timeout[0] / 1_000, read=_configured_timeout[1] / 1_000)
|
||||
|
||||
if 'Content-Type' not in headers:
|
||||
headers['Content-Type'] = 'application/json'
|
||||
|
||||
if self.configuration.debug:
|
||||
_BaseRESTClient.log_request(method, f"{url}{'' if query_params is None else '?' + urlencode(query_params)}")
|
||||
_BaseRESTClient.log_headers(headers, '>>>')
|
||||
_BaseRESTClient.log_body(body, '>>>')
|
||||
|
||||
try:
|
||||
# For `POST`, `PUT`, `PATCH`, `OPTIONS`, `DELETE`
|
||||
if method in ['POST', 'PUT', 'PATCH', 'OPTIONS', 'DELETE']:
|
||||
if query_params:
|
||||
url += '?' + urlencode(query_params)
|
||||
if re.search('json', headers['Content-Type'], re.IGNORECASE):
|
||||
request_body = None
|
||||
if body is not None:
|
||||
request_body = json.dumps(body)
|
||||
r = self.pool_manager.request(
|
||||
method, url,
|
||||
body=request_body,
|
||||
preload_content=_preload_content,
|
||||
timeout=timeout,
|
||||
headers=headers,
|
||||
**urlopen_kw)
|
||||
elif headers['Content-Type'] == 'application/x-www-form-urlencoded': # noqa: E501
|
||||
r = self.pool_manager.request(
|
||||
method, url,
|
||||
fields=post_params,
|
||||
encode_multipart=False,
|
||||
preload_content=_preload_content,
|
||||
timeout=timeout,
|
||||
headers=headers,
|
||||
**urlopen_kw)
|
||||
elif headers['Content-Type'] == 'multipart/form-data':
|
||||
# must del headers['Content-Type'], or the correct
|
||||
# Content-Type which generated by urllib3 will be
|
||||
# overwritten.
|
||||
del headers['Content-Type']
|
||||
r = self.pool_manager.request(
|
||||
method, url,
|
||||
fields=post_params,
|
||||
encode_multipart=True,
|
||||
preload_content=_preload_content,
|
||||
timeout=timeout,
|
||||
headers=headers,
|
||||
**urlopen_kw)
|
||||
# Pass a `string` parameter directly in the body to support
|
||||
# other content types than Json when `body` argument is
|
||||
# provided in serialized form
|
||||
elif isinstance(body, str) or isinstance(body, bytes):
|
||||
request_body = body
|
||||
r = self.pool_manager.request(
|
||||
method, url,
|
||||
body=request_body,
|
||||
preload_content=_preload_content,
|
||||
timeout=timeout,
|
||||
headers=headers,
|
||||
**urlopen_kw)
|
||||
else:
|
||||
# Cannot generate the request from given parameters
|
||||
msg = """Cannot prepare a request message for provided
|
||||
arguments. Please check that your arguments match
|
||||
declared content type."""
|
||||
raise ApiException(status=0, reason=msg)
|
||||
# For `GET`, `HEAD`
|
||||
else:
|
||||
r = self.pool_manager.request(method, url,
|
||||
fields=query_params,
|
||||
preload_content=_preload_content,
|
||||
timeout=timeout,
|
||||
headers=headers,
|
||||
**urlopen_kw)
|
||||
except urllib3.exceptions.SSLError as e:
|
||||
msg = "{0}\n{1}".format(type(e).__name__, str(e))
|
||||
raise ApiException(status=0, reason=msg)
|
||||
|
||||
if _preload_content:
|
||||
r = RESTResponse(r)
|
||||
|
||||
# In the python 3, the response.data is bytes.
|
||||
# we need to decode it to string.
|
||||
r.data = r.data.decode('utf8')
|
||||
|
||||
if self.configuration.debug:
|
||||
_BaseRESTClient.log_response(r.status)
|
||||
if hasattr(r, 'headers'):
|
||||
_BaseRESTClient.log_headers(r.headers, '<<<')
|
||||
if hasattr(r, 'urllib3_response'):
|
||||
_BaseRESTClient.log_headers(r.urllib3_response.headers, '<<<')
|
||||
_BaseRESTClient.log_body(r.data, '<<<')
|
||||
|
||||
if not 200 <= r.status <= 299:
|
||||
raise ApiException(http_resp=r)
|
||||
|
||||
return r
|
||||
|
||||
def GET(self, url, headers=None, query_params=None, _preload_content=True,
|
||||
_request_timeout=None, **urlopen_kw):
|
||||
"""Perform GET HTTP request."""
|
||||
return self.request("GET", url,
|
||||
headers=headers,
|
||||
_preload_content=_preload_content,
|
||||
_request_timeout=_request_timeout,
|
||||
query_params=query_params,
|
||||
**urlopen_kw)
|
||||
|
||||
def HEAD(self, url, headers=None, query_params=None, _preload_content=True,
|
||||
_request_timeout=None, **urlopen_kw):
|
||||
"""Perform HEAD HTTP request."""
|
||||
return self.request("HEAD", url,
|
||||
headers=headers,
|
||||
_preload_content=_preload_content,
|
||||
_request_timeout=_request_timeout,
|
||||
query_params=query_params,
|
||||
**urlopen_kw)
|
||||
|
||||
def OPTIONS(self, url, headers=None, query_params=None, post_params=None,
|
||||
body=None, _preload_content=True, _request_timeout=None, **urlopen_kw):
|
||||
"""Perform OPTIONS HTTP request."""
|
||||
return self.request("OPTIONS", url,
|
||||
headers=headers,
|
||||
query_params=query_params,
|
||||
post_params=post_params,
|
||||
_preload_content=_preload_content,
|
||||
_request_timeout=_request_timeout,
|
||||
body=body,
|
||||
**urlopen_kw)
|
||||
|
||||
def DELETE(self, url, headers=None, query_params=None, body=None,
|
||||
_preload_content=True, _request_timeout=None, **urlopen_kw):
|
||||
"""Perform DELETE HTTP request."""
|
||||
return self.request("DELETE", url,
|
||||
headers=headers,
|
||||
query_params=query_params,
|
||||
_preload_content=_preload_content,
|
||||
_request_timeout=_request_timeout,
|
||||
body=body,
|
||||
**urlopen_kw)
|
||||
|
||||
def POST(self, url, headers=None, query_params=None, post_params=None,
|
||||
body=None, _preload_content=True, _request_timeout=None, **urlopen_kw):
|
||||
"""Perform POST HTTP request."""
|
||||
return self.request("POST", url,
|
||||
headers=headers,
|
||||
query_params=query_params,
|
||||
post_params=post_params,
|
||||
_preload_content=_preload_content,
|
||||
_request_timeout=_request_timeout,
|
||||
body=body,
|
||||
**urlopen_kw)
|
||||
|
||||
def PUT(self, url, headers=None, query_params=None, post_params=None,
|
||||
body=None, _preload_content=True, _request_timeout=None, **urlopen_kw):
|
||||
"""Perform PUT HTTP request."""
|
||||
return self.request("PUT", url,
|
||||
headers=headers,
|
||||
query_params=query_params,
|
||||
post_params=post_params,
|
||||
_preload_content=_preload_content,
|
||||
_request_timeout=_request_timeout,
|
||||
body=body,
|
||||
**urlopen_kw)
|
||||
|
||||
def PATCH(self, url, headers=None, query_params=None, post_params=None,
|
||||
body=None, _preload_content=True, _request_timeout=None, **urlopen_kw):
|
||||
"""Perform PATCH HTTP request."""
|
||||
return self.request("PATCH", url,
|
||||
headers=headers,
|
||||
query_params=query_params,
|
||||
post_params=post_params,
|
||||
_preload_content=_preload_content,
|
||||
_request_timeout=_request_timeout,
|
||||
body=body,
|
||||
**urlopen_kw)
|
||||
|
||||
def __getstate__(self):
|
||||
"""Return a dict of attributes that you want to pickle."""
|
||||
state = self.__dict__.copy()
|
||||
# Remove Pool managaer
|
||||
del state['pool_manager']
|
||||
return state
|
||||
|
||||
def __setstate__(self, state):
|
||||
"""Set your object with the provided dict."""
|
||||
self.__dict__.update(state)
|
||||
# Init Pool manager
|
||||
self.__init__(self.configuration, self.pools_size, self.maxsize, self.retries)
|
||||
@@ -0,0 +1,56 @@
|
||||
# flake8: noqa
|
||||
|
||||
"""
|
||||
InfluxDB OSS API Service.
|
||||
|
||||
The InfluxDB v2 API provides a programmatic interface for all interactions with InfluxDB. Access the InfluxDB API using the `/api/v2/` endpoint. # noqa: E501
|
||||
|
||||
OpenAPI spec version: 2.0.0
|
||||
Generated by: https://openapi-generator.tech
|
||||
"""
|
||||
|
||||
|
||||
from __future__ import absolute_import
|
||||
|
||||
# import apis into api package
|
||||
from influxdb_client.service.authorizations_service import AuthorizationsService
|
||||
from influxdb_client.service.backup_service import BackupService
|
||||
from influxdb_client.service.bucket_schemas_service import BucketSchemasService
|
||||
from influxdb_client.service.buckets_service import BucketsService
|
||||
from influxdb_client.service.cells_service import CellsService
|
||||
from influxdb_client.service.checks_service import ChecksService
|
||||
from influxdb_client.service.config_service import ConfigService
|
||||
from influxdb_client.service.dbr_ps_service import DBRPsService
|
||||
from influxdb_client.service.dashboards_service import DashboardsService
|
||||
from influxdb_client.service.delete_service import DeleteService
|
||||
from influxdb_client.service.health_service import HealthService
|
||||
from influxdb_client.service.invokable_scripts_service import InvokableScriptsService
|
||||
from influxdb_client.service.labels_service import LabelsService
|
||||
from influxdb_client.service.legacy_authorizations_service import LegacyAuthorizationsService
|
||||
from influxdb_client.service.metrics_service import MetricsService
|
||||
from influxdb_client.service.notification_endpoints_service import NotificationEndpointsService
|
||||
from influxdb_client.service.notification_rules_service import NotificationRulesService
|
||||
from influxdb_client.service.organizations_service import OrganizationsService
|
||||
from influxdb_client.service.ping_service import PingService
|
||||
from influxdb_client.service.query_service import QueryService
|
||||
from influxdb_client.service.ready_service import ReadyService
|
||||
from influxdb_client.service.remote_connections_service import RemoteConnectionsService
|
||||
from influxdb_client.service.replications_service import ReplicationsService
|
||||
from influxdb_client.service.resources_service import ResourcesService
|
||||
from influxdb_client.service.restore_service import RestoreService
|
||||
from influxdb_client.service.routes_service import RoutesService
|
||||
from influxdb_client.service.rules_service import RulesService
|
||||
from influxdb_client.service.scraper_targets_service import ScraperTargetsService
|
||||
from influxdb_client.service.secrets_service import SecretsService
|
||||
from influxdb_client.service.setup_service import SetupService
|
||||
from influxdb_client.service.signin_service import SigninService
|
||||
from influxdb_client.service.signout_service import SignoutService
|
||||
from influxdb_client.service.sources_service import SourcesService
|
||||
from influxdb_client.service.tasks_service import TasksService
|
||||
from influxdb_client.service.telegraf_plugins_service import TelegrafPluginsService
|
||||
from influxdb_client.service.telegrafs_service import TelegrafsService
|
||||
from influxdb_client.service.templates_service import TemplatesService
|
||||
from influxdb_client.service.users_service import UsersService
|
||||
from influxdb_client.service.variables_service import VariablesService
|
||||
from influxdb_client.service.views_service import ViewsService
|
||||
from influxdb_client.service.write_service import WriteService
|
||||
@@ -0,0 +1,556 @@
|
||||
"""Commons function for Sync and Async client."""
|
||||
from __future__ import absolute_import
|
||||
|
||||
import base64
|
||||
import configparser
|
||||
import logging
|
||||
import os
|
||||
from datetime import datetime, timedelta
|
||||
from typing import List, Generator, Any, Union, Iterable, AsyncGenerator
|
||||
|
||||
from urllib3 import HTTPResponse
|
||||
|
||||
from influxdb_client import Configuration, Dialect, Query, OptionStatement, VariableAssignment, Identifier, \
|
||||
Expression, BooleanLiteral, IntegerLiteral, FloatLiteral, DateTimeLiteral, UnaryExpression, DurationLiteral, \
|
||||
Duration, StringLiteral, ArrayExpression, ImportDeclaration, MemberExpression, MemberAssignment, File, \
|
||||
WriteService, QueryService, DeleteService, DeletePredicateRequest
|
||||
from influxdb_client.client.flux_csv_parser import FluxResponseMetadataMode, FluxCsvParser, FluxSerializationMode
|
||||
from influxdb_client.client.flux_table import FluxRecord, TableList, CSVIterator
|
||||
from influxdb_client.client.util.date_utils import get_date_helper
|
||||
from influxdb_client.client.util.helpers import get_org_query_param
|
||||
from influxdb_client.client.warnings import MissingPivotFunction
|
||||
from influxdb_client.client.write.dataframe_serializer import DataframeSerializer
|
||||
from influxdb_client.rest import _UTF_8_encoding
|
||||
|
||||
try:
|
||||
import dataclasses
|
||||
|
||||
_HAS_DATACLASS = True
|
||||
except ModuleNotFoundError:
|
||||
_HAS_DATACLASS = False
|
||||
|
||||
LOGGERS_NAMES = [
|
||||
'influxdb_client.client.influxdb_client',
|
||||
'influxdb_client.client.influxdb_client_async',
|
||||
'influxdb_client.client.write_api',
|
||||
'influxdb_client.client.write_api_async',
|
||||
'influxdb_client.client.write.retry',
|
||||
'influxdb_client.client.write.dataframe_serializer',
|
||||
'influxdb_client.client.util.multiprocessing_helper',
|
||||
'influxdb_client.client.http',
|
||||
'influxdb_client.client.exceptions',
|
||||
]
|
||||
|
||||
|
||||
# noinspection PyMethodMayBeStatic
|
||||
class _BaseClient(object):
|
||||
def __init__(self, url, token, debug=None, timeout=10_000, enable_gzip=False, org: str = None,
|
||||
default_tags: dict = None, http_client_logger: str = None, **kwargs) -> None:
|
||||
self.url = url
|
||||
self.token = token
|
||||
self.org = org
|
||||
|
||||
self.default_tags = default_tags
|
||||
|
||||
self.conf = _Configuration()
|
||||
if not isinstance(self.url, str):
|
||||
raise ValueError('"url" attribute is not str instance')
|
||||
if self.url.endswith("/"):
|
||||
self.conf.host = self.url[:-1]
|
||||
else:
|
||||
self.conf.host = self.url
|
||||
self.conf.enable_gzip = enable_gzip
|
||||
self.conf.verify_ssl = kwargs.get('verify_ssl', True)
|
||||
self.conf.ssl_ca_cert = kwargs.get('ssl_ca_cert', None)
|
||||
self.conf.cert_file = kwargs.get('cert_file', None)
|
||||
self.conf.cert_key_file = kwargs.get('cert_key_file', None)
|
||||
self.conf.cert_key_password = kwargs.get('cert_key_password', None)
|
||||
self.conf.ssl_context = kwargs.get('ssl_context', None)
|
||||
self.conf.proxy = kwargs.get('proxy', None)
|
||||
self.conf.proxy_headers = kwargs.get('proxy_headers', None)
|
||||
self.conf.connection_pool_maxsize = kwargs.get('connection_pool_maxsize', self.conf.connection_pool_maxsize)
|
||||
self.conf.timeout = timeout
|
||||
# logging
|
||||
self.conf.loggers["http_client_logger"] = logging.getLogger(http_client_logger)
|
||||
for client_logger in LOGGERS_NAMES:
|
||||
self.conf.loggers[client_logger] = logging.getLogger(client_logger)
|
||||
self.conf.debug = debug
|
||||
|
||||
self.conf.username = kwargs.get('username', None)
|
||||
self.conf.password = kwargs.get('password', None)
|
||||
# defaults
|
||||
self.auth_header_name = None
|
||||
self.auth_header_value = None
|
||||
# by token
|
||||
if self.token:
|
||||
self.auth_header_name = "Authorization"
|
||||
self.auth_header_value = "Token " + self.token
|
||||
# by HTTP basic
|
||||
auth_basic = kwargs.get('auth_basic', False)
|
||||
if auth_basic:
|
||||
self.auth_header_name = "Authorization"
|
||||
self.auth_header_value = "Basic " + base64.b64encode(token.encode()).decode()
|
||||
# by username, password
|
||||
if self.conf.username and self.conf.password:
|
||||
self.auth_header_name = None
|
||||
self.auth_header_value = None
|
||||
|
||||
self.retries = kwargs.get('retries', False)
|
||||
|
||||
self.profilers = kwargs.get('profilers', None)
|
||||
pass
|
||||
|
||||
@classmethod
|
||||
def _from_config_file(cls, config_file: str = "config.ini", debug=None, enable_gzip=False, **kwargs):
|
||||
config = configparser.ConfigParser()
|
||||
config_name = kwargs.get('config_name', 'influx2')
|
||||
is_json = False
|
||||
try:
|
||||
config.read(config_file)
|
||||
except configparser.ParsingError:
|
||||
with open(config_file) as json_file:
|
||||
import json
|
||||
config = json.load(json_file)
|
||||
is_json = True
|
||||
|
||||
def _config_value(key: str):
|
||||
value = str(config[key]) if is_json else config[config_name][key]
|
||||
return value.strip('"')
|
||||
|
||||
def _has_option(key: str):
|
||||
return key in config if is_json else config.has_option(config_name, key)
|
||||
|
||||
def _has_section(key: str):
|
||||
return key in config if is_json else config.has_section(key)
|
||||
|
||||
url = _config_value('url')
|
||||
token = _config_value('token')
|
||||
|
||||
timeout = None
|
||||
if _has_option('timeout'):
|
||||
timeout = _config_value('timeout')
|
||||
|
||||
org = None
|
||||
if _has_option('org'):
|
||||
org = _config_value('org')
|
||||
|
||||
verify_ssl = True
|
||||
if _has_option('verify_ssl'):
|
||||
verify_ssl = _config_value('verify_ssl')
|
||||
|
||||
ssl_ca_cert = None
|
||||
if _has_option('ssl_ca_cert'):
|
||||
ssl_ca_cert = _config_value('ssl_ca_cert')
|
||||
|
||||
cert_file = None
|
||||
if _has_option('cert_file'):
|
||||
cert_file = _config_value('cert_file')
|
||||
|
||||
cert_key_file = None
|
||||
if _has_option('cert_key_file'):
|
||||
cert_key_file = _config_value('cert_key_file')
|
||||
|
||||
cert_key_password = None
|
||||
if _has_option('cert_key_password'):
|
||||
cert_key_password = _config_value('cert_key_password')
|
||||
|
||||
connection_pool_maxsize = None
|
||||
if _has_option('connection_pool_maxsize'):
|
||||
connection_pool_maxsize = _config_value('connection_pool_maxsize')
|
||||
|
||||
auth_basic = False
|
||||
if _has_option('auth_basic'):
|
||||
auth_basic = _config_value('auth_basic')
|
||||
|
||||
default_tags = None
|
||||
if _has_section('tags'):
|
||||
if is_json:
|
||||
default_tags = config['tags']
|
||||
else:
|
||||
tags = {k: v.strip('"') for k, v in config.items('tags')}
|
||||
default_tags = dict(tags)
|
||||
|
||||
profilers = None
|
||||
if _has_option('profilers'):
|
||||
profilers = [x.strip() for x in _config_value('profilers').split(',')]
|
||||
|
||||
proxy = None
|
||||
if _has_option('proxy'):
|
||||
proxy = _config_value('proxy')
|
||||
|
||||
return cls(url, token, debug=debug, timeout=_to_int(timeout), org=org, default_tags=default_tags,
|
||||
enable_gzip=enable_gzip, verify_ssl=_to_bool(verify_ssl), ssl_ca_cert=ssl_ca_cert,
|
||||
cert_file=cert_file, cert_key_file=cert_key_file, cert_key_password=cert_key_password,
|
||||
connection_pool_maxsize=_to_int(connection_pool_maxsize), auth_basic=_to_bool(auth_basic),
|
||||
profilers=profilers, proxy=proxy, **kwargs)
|
||||
|
||||
@classmethod
|
||||
def _from_env_properties(cls, debug=None, enable_gzip=False, **kwargs):
|
||||
url = os.getenv('INFLUXDB_V2_URL', "http://localhost:8086")
|
||||
token = os.getenv('INFLUXDB_V2_TOKEN', "my-token")
|
||||
timeout = os.getenv('INFLUXDB_V2_TIMEOUT', "10000")
|
||||
org = os.getenv('INFLUXDB_V2_ORG', "my-org")
|
||||
verify_ssl = os.getenv('INFLUXDB_V2_VERIFY_SSL', "True")
|
||||
ssl_ca_cert = os.getenv('INFLUXDB_V2_SSL_CA_CERT', None)
|
||||
cert_file = os.getenv('INFLUXDB_V2_CERT_FILE', None)
|
||||
cert_key_file = os.getenv('INFLUXDB_V2_CERT_KEY_FILE', None)
|
||||
cert_key_password = os.getenv('INFLUXDB_V2_CERT_KEY_PASSWORD', None)
|
||||
connection_pool_maxsize = os.getenv('INFLUXDB_V2_CONNECTION_POOL_MAXSIZE', None)
|
||||
auth_basic = os.getenv('INFLUXDB_V2_AUTH_BASIC', "False")
|
||||
|
||||
prof = os.getenv("INFLUXDB_V2_PROFILERS", None)
|
||||
profilers = None
|
||||
if prof is not None:
|
||||
profilers = [x.strip() for x in prof.split(',')]
|
||||
|
||||
default_tags = dict()
|
||||
|
||||
for key, value in os.environ.items():
|
||||
if key.startswith("INFLUXDB_V2_TAG_"):
|
||||
default_tags[key[16:].lower()] = value
|
||||
|
||||
return cls(url, token, debug=debug, timeout=_to_int(timeout), org=org, default_tags=default_tags,
|
||||
enable_gzip=enable_gzip, verify_ssl=_to_bool(verify_ssl), ssl_ca_cert=ssl_ca_cert,
|
||||
cert_file=cert_file, cert_key_file=cert_key_file, cert_key_password=cert_key_password,
|
||||
connection_pool_maxsize=_to_int(connection_pool_maxsize), auth_basic=_to_bool(auth_basic),
|
||||
profilers=profilers, **kwargs)
|
||||
|
||||
|
||||
# noinspection PyMethodMayBeStatic
|
||||
class _BaseQueryApi(object):
|
||||
default_dialect = Dialect(header=True, delimiter=",", comment_prefix="#",
|
||||
annotations=["datatype", "group", "default"], date_time_format="RFC3339")
|
||||
|
||||
def __init__(self, influxdb_client, query_options=None):
|
||||
from influxdb_client.client.query_api import QueryOptions
|
||||
self._query_options = QueryOptions() if query_options is None else query_options
|
||||
self._influxdb_client = influxdb_client
|
||||
self._query_api = QueryService(influxdb_client.api_client)
|
||||
|
||||
"""Base implementation for Queryable API."""
|
||||
|
||||
def _to_tables(self, response, query_options=None, response_metadata_mode:
|
||||
FluxResponseMetadataMode = FluxResponseMetadataMode.full) -> TableList:
|
||||
"""
|
||||
Parse HTTP response to TableList.
|
||||
|
||||
:param response: HTTP response from an HTTP client. Expected type: `urllib3.response.HTTPResponse`.
|
||||
"""
|
||||
_parser = self._to_tables_parser(response, query_options, response_metadata_mode)
|
||||
list(_parser.generator())
|
||||
return _parser.table_list()
|
||||
|
||||
async def _to_tables_async(self, response, query_options=None, response_metadata_mode:
|
||||
FluxResponseMetadataMode = FluxResponseMetadataMode.full) -> TableList:
|
||||
"""
|
||||
Parse HTTP response to TableList.
|
||||
|
||||
:param response: HTTP response from an HTTP client. Expected type: `aiohttp.client_reqrep.ClientResponse`.
|
||||
"""
|
||||
async with self._to_tables_parser(response, query_options, response_metadata_mode) as parser:
|
||||
async for _ in parser.generator_async():
|
||||
pass
|
||||
return parser.table_list()
|
||||
|
||||
def _to_csv(self, response: HTTPResponse) -> CSVIterator:
|
||||
"""Parse HTTP response to CSV."""
|
||||
return CSVIterator(response)
|
||||
|
||||
def _to_flux_record_stream(self, response, query_options=None,
|
||||
response_metadata_mode: FluxResponseMetadataMode = FluxResponseMetadataMode.full) -> \
|
||||
Generator[FluxRecord, Any, None]:
|
||||
"""
|
||||
Parse HTTP response to FluxRecord stream.
|
||||
|
||||
:param response: HTTP response from an HTTP client. Expected type: `urllib3.response.HTTPResponse`.
|
||||
"""
|
||||
_parser = self._to_flux_record_stream_parser(query_options, response, response_metadata_mode)
|
||||
return _parser.generator()
|
||||
|
||||
async def _to_flux_record_stream_async(self, response, query_options=None, response_metadata_mode:
|
||||
FluxResponseMetadataMode = FluxResponseMetadataMode.full) -> \
|
||||
AsyncGenerator['FluxRecord', None]:
|
||||
"""
|
||||
Parse HTTP response to FluxRecord stream.
|
||||
|
||||
:param response: HTTP response from an HTTP client. Expected type: `aiohttp.client_reqrep.ClientResponse`.
|
||||
"""
|
||||
_parser = self._to_flux_record_stream_parser(query_options, response, response_metadata_mode)
|
||||
return (await _parser.__aenter__()).generator_async()
|
||||
|
||||
def _to_data_frame_stream(self, data_frame_index, response, query_options=None,
|
||||
response_metadata_mode: FluxResponseMetadataMode = FluxResponseMetadataMode.full,
|
||||
use_extension_dtypes=False):
|
||||
"""
|
||||
Parse HTTP response to DataFrame stream.
|
||||
|
||||
:param response: HTTP response from an HTTP client. Expected type: `urllib3.response.HTTPResponse`.
|
||||
"""
|
||||
_parser = self._to_data_frame_stream_parser(data_frame_index, query_options, response, response_metadata_mode,
|
||||
use_extension_dtypes)
|
||||
return _parser.generator()
|
||||
|
||||
async def _to_data_frame_stream_async(self, data_frame_index, response, query_options=None, response_metadata_mode:
|
||||
FluxResponseMetadataMode = FluxResponseMetadataMode.full,
|
||||
use_extension_dtypes=False):
|
||||
"""
|
||||
Parse HTTP response to DataFrame stream.
|
||||
|
||||
:param response: HTTP response from an HTTP client. Expected type: `aiohttp.client_reqrep.ClientResponse`.
|
||||
"""
|
||||
_parser = self._to_data_frame_stream_parser(data_frame_index, query_options, response, response_metadata_mode,
|
||||
use_extension_dtypes)
|
||||
return (await _parser.__aenter__()).generator_async()
|
||||
|
||||
def _to_tables_parser(self, response, query_options, response_metadata_mode):
|
||||
return FluxCsvParser(response=response, serialization_mode=FluxSerializationMode.tables,
|
||||
query_options=query_options, response_metadata_mode=response_metadata_mode)
|
||||
|
||||
def _to_flux_record_stream_parser(self, query_options, response, response_metadata_mode):
|
||||
return FluxCsvParser(response=response, serialization_mode=FluxSerializationMode.stream,
|
||||
query_options=query_options, response_metadata_mode=response_metadata_mode)
|
||||
|
||||
def _to_data_frame_stream_parser(self, data_frame_index, query_options, response, response_metadata_mode,
|
||||
use_extension_dtypes):
|
||||
return FluxCsvParser(response=response, serialization_mode=FluxSerializationMode.dataFrame,
|
||||
data_frame_index=data_frame_index, query_options=query_options,
|
||||
response_metadata_mode=response_metadata_mode,
|
||||
use_extension_dtypes=use_extension_dtypes)
|
||||
|
||||
def _to_data_frames(self, _generator):
|
||||
"""Parse stream of DataFrames into expected type."""
|
||||
from ..extras import pd
|
||||
if isinstance(_generator, list):
|
||||
_dataFrames = _generator
|
||||
else:
|
||||
_dataFrames = list(_generator)
|
||||
|
||||
if len(_dataFrames) == 0:
|
||||
return pd.DataFrame(columns=[], index=None)
|
||||
elif len(_dataFrames) == 1:
|
||||
return _dataFrames[0]
|
||||
else:
|
||||
return _dataFrames
|
||||
|
||||
def _org_param(self, org):
|
||||
return get_org_query_param(org=org, client=self._influxdb_client)
|
||||
|
||||
def _get_query_options(self):
|
||||
if self._query_options and self._query_options.profilers:
|
||||
return self._query_options
|
||||
elif self._influxdb_client.profilers:
|
||||
from influxdb_client.client.query_api import QueryOptions
|
||||
return QueryOptions(profilers=self._influxdb_client.profilers)
|
||||
|
||||
def _create_query(self, query, dialect=default_dialect, params: dict = None, **kwargs):
|
||||
query_options = self._get_query_options()
|
||||
profilers = query_options.profilers if query_options is not None else None
|
||||
q = Query(query=query, dialect=dialect, extern=_BaseQueryApi._build_flux_ast(params, profilers))
|
||||
|
||||
if profilers:
|
||||
print("\n===============")
|
||||
print("Profiler: query")
|
||||
print("===============")
|
||||
print(query)
|
||||
|
||||
if kwargs.get('dataframe_query', False):
|
||||
MissingPivotFunction.print_warning(query)
|
||||
|
||||
return q
|
||||
|
||||
@staticmethod
|
||||
def _params_to_extern_ast(params: dict) -> List['OptionStatement']:
|
||||
|
||||
statements = []
|
||||
for key, value in params.items():
|
||||
expression = _BaseQueryApi._parm_to_extern_ast(value)
|
||||
if expression is None:
|
||||
continue
|
||||
|
||||
statements.append(OptionStatement("OptionStatement",
|
||||
VariableAssignment("VariableAssignment", Identifier("Identifier", key),
|
||||
expression)))
|
||||
return statements
|
||||
|
||||
@staticmethod
|
||||
def _parm_to_extern_ast(value) -> Union[Expression, None]:
|
||||
if value is None:
|
||||
return None
|
||||
if isinstance(value, bool):
|
||||
return BooleanLiteral("BooleanLiteral", value)
|
||||
elif isinstance(value, int):
|
||||
return IntegerLiteral("IntegerLiteral", str(value))
|
||||
elif isinstance(value, float):
|
||||
return FloatLiteral("FloatLiteral", value)
|
||||
elif isinstance(value, datetime):
|
||||
value = get_date_helper().to_utc(value)
|
||||
nanoseconds = getattr(value, 'nanosecond', 0)
|
||||
fraction = f'{(value.microsecond * 1000 + nanoseconds):09d}'
|
||||
return DateTimeLiteral("DateTimeLiteral", value.strftime('%Y-%m-%dT%H:%M:%S.') + fraction + 'Z')
|
||||
elif isinstance(value, timedelta):
|
||||
_micro_delta = int(value / timedelta(microseconds=1))
|
||||
if _micro_delta < 0:
|
||||
return UnaryExpression("UnaryExpression", argument=DurationLiteral("DurationLiteral", [
|
||||
Duration(magnitude=-_micro_delta, unit="us")]), operator="-")
|
||||
else:
|
||||
return DurationLiteral("DurationLiteral", [Duration(magnitude=_micro_delta, unit="us")])
|
||||
elif isinstance(value, str):
|
||||
return StringLiteral("StringLiteral", str(value))
|
||||
elif isinstance(value, Iterable):
|
||||
return ArrayExpression("ArrayExpression",
|
||||
elements=list(map(lambda it: _BaseQueryApi._parm_to_extern_ast(it), value)))
|
||||
else:
|
||||
return value
|
||||
|
||||
@staticmethod
|
||||
def _build_flux_ast(params: dict = None, profilers: List[str] = None):
|
||||
|
||||
imports = []
|
||||
body = []
|
||||
|
||||
if profilers is not None and len(profilers) > 0:
|
||||
imports.append(ImportDeclaration(
|
||||
"ImportDeclaration",
|
||||
path=StringLiteral("StringLiteral", "profiler")))
|
||||
|
||||
elements = []
|
||||
for profiler in profilers:
|
||||
elements.append(StringLiteral("StringLiteral", value=profiler))
|
||||
|
||||
member = MemberExpression(
|
||||
"MemberExpression",
|
||||
object=Identifier("Identifier", "profiler"),
|
||||
_property=Identifier("Identifier", "enabledProfilers"))
|
||||
|
||||
prof = OptionStatement(
|
||||
"OptionStatement",
|
||||
assignment=MemberAssignment(
|
||||
"MemberAssignment",
|
||||
member=member,
|
||||
init=ArrayExpression(
|
||||
"ArrayExpression",
|
||||
elements=elements)))
|
||||
|
||||
body.append(prof)
|
||||
|
||||
if params is not None:
|
||||
body.extend(_BaseQueryApi._params_to_extern_ast(params))
|
||||
|
||||
return File(package=None, name=None, type=None, imports=imports, body=body)
|
||||
|
||||
|
||||
class _BaseWriteApi(object):
|
||||
def __init__(self, influxdb_client, point_settings=None):
|
||||
self._influxdb_client = influxdb_client
|
||||
self._point_settings = point_settings
|
||||
self._write_service = WriteService(influxdb_client.api_client)
|
||||
if influxdb_client.default_tags:
|
||||
for key, value in influxdb_client.default_tags.items():
|
||||
self._point_settings.add_default_tag(key, value)
|
||||
|
||||
def _append_default_tag(self, key, val, record):
|
||||
from influxdb_client import Point
|
||||
if isinstance(record, bytes) or isinstance(record, str):
|
||||
pass
|
||||
elif isinstance(record, Point):
|
||||
record.tag(key, val)
|
||||
elif isinstance(record, dict):
|
||||
record.setdefault("tags", {})
|
||||
record.get("tags")[key] = val
|
||||
elif isinstance(record, Iterable):
|
||||
for item in record:
|
||||
self._append_default_tag(key, val, item)
|
||||
|
||||
def _append_default_tags(self, record):
|
||||
if self._point_settings.defaultTags and record is not None:
|
||||
for key, val in self._point_settings.defaultTags.items():
|
||||
self._append_default_tag(key, val, record)
|
||||
|
||||
def _serialize(self, record, write_precision, payload, **kwargs):
|
||||
from influxdb_client import Point
|
||||
if isinstance(record, bytes):
|
||||
payload[write_precision].append(record)
|
||||
|
||||
elif isinstance(record, str):
|
||||
self._serialize(record.encode(_UTF_8_encoding), write_precision, payload, **kwargs)
|
||||
|
||||
elif isinstance(record, Point):
|
||||
precision_from_point = kwargs.get('precision_from_point', True)
|
||||
precision = record.write_precision if precision_from_point else write_precision
|
||||
self._serialize(record.to_line_protocol(precision=precision), precision, payload, **kwargs)
|
||||
|
||||
elif isinstance(record, dict):
|
||||
self._serialize(Point.from_dict(record, write_precision=write_precision, **kwargs),
|
||||
write_precision, payload, **kwargs)
|
||||
elif 'DataFrame' in type(record).__name__:
|
||||
serializer = DataframeSerializer(record, self._point_settings, write_precision, **kwargs)
|
||||
self._serialize(serializer.serialize(), write_precision, payload, **kwargs)
|
||||
elif hasattr(record, "_asdict"):
|
||||
# noinspection PyProtectedMember
|
||||
self._serialize(record._asdict(), write_precision, payload, **kwargs)
|
||||
elif _HAS_DATACLASS and dataclasses.is_dataclass(record):
|
||||
self._serialize(dataclasses.asdict(record), write_precision, payload, **kwargs)
|
||||
elif isinstance(record, Iterable):
|
||||
for item in record:
|
||||
self._serialize(item, write_precision, payload, **kwargs)
|
||||
|
||||
|
||||
# noinspection PyMethodMayBeStatic
|
||||
class _BaseDeleteApi(object):
|
||||
def __init__(self, influxdb_client):
|
||||
self._influxdb_client = influxdb_client
|
||||
self._service = DeleteService(influxdb_client.api_client)
|
||||
|
||||
def _prepare_predicate_request(self, start, stop, predicate):
|
||||
date_helper = get_date_helper()
|
||||
if isinstance(start, datetime):
|
||||
start = date_helper.to_utc(start)
|
||||
if isinstance(stop, datetime):
|
||||
stop = date_helper.to_utc(stop)
|
||||
predicate_request = DeletePredicateRequest(start=start, stop=stop, predicate=predicate)
|
||||
return predicate_request
|
||||
|
||||
|
||||
class _Configuration(Configuration):
|
||||
def __init__(self):
|
||||
Configuration.__init__(self)
|
||||
self.enable_gzip = False
|
||||
self.username = None
|
||||
self.password = None
|
||||
|
||||
def update_request_header_params(self, path: str, params: dict):
|
||||
super().update_request_header_params(path, params)
|
||||
if self.enable_gzip:
|
||||
# GZIP Request
|
||||
if path == '/api/v2/write':
|
||||
params["Content-Encoding"] = "gzip"
|
||||
params["Accept-Encoding"] = "identity"
|
||||
pass
|
||||
# GZIP Response
|
||||
if path == '/api/v2/query':
|
||||
# params["Content-Encoding"] = "gzip"
|
||||
params["Accept-Encoding"] = "gzip"
|
||||
pass
|
||||
pass
|
||||
pass
|
||||
|
||||
def update_request_body(self, path: str, body):
|
||||
_body = super().update_request_body(path, body)
|
||||
if self.enable_gzip:
|
||||
# GZIP Request
|
||||
if path == '/api/v2/write':
|
||||
import gzip
|
||||
if isinstance(_body, bytes):
|
||||
return gzip.compress(data=_body)
|
||||
else:
|
||||
return gzip.compress(bytes(_body, _UTF_8_encoding))
|
||||
|
||||
return _body
|
||||
|
||||
|
||||
def _to_bool(bool_value):
|
||||
return str(bool_value).lower() in ("yes", "true")
|
||||
|
||||
|
||||
def _to_int(int_value):
|
||||
return int(int_value) if int_value is not None else None
|
||||
@@ -0,0 +1,66 @@
|
||||
|
||||
|
||||
class _Page:
|
||||
def __init__(self, values, has_next, next_after):
|
||||
self.has_next = has_next
|
||||
self.values = values
|
||||
self.next_after = next_after
|
||||
|
||||
@staticmethod
|
||||
def empty():
|
||||
return _Page([], False, None)
|
||||
|
||||
@staticmethod
|
||||
def initial(after):
|
||||
return _Page([], True, after)
|
||||
|
||||
|
||||
class _PageIterator:
|
||||
def __init__(self, page: _Page, get_next_page):
|
||||
self.page = page
|
||||
self.get_next_page = get_next_page
|
||||
|
||||
def __iter__(self):
|
||||
return self
|
||||
|
||||
def __next__(self):
|
||||
if not self.page.values:
|
||||
if self.page.has_next:
|
||||
self.page = self.get_next_page(self.page)
|
||||
if not self.page.values:
|
||||
raise StopIteration
|
||||
return self.page.values.pop(0)
|
||||
|
||||
|
||||
class _Paginated:
|
||||
def __init__(self, paginated_getter, pluck_page_resources_from_response):
|
||||
self.paginated_getter = paginated_getter
|
||||
self.pluck_page_resources_from_response = pluck_page_resources_from_response
|
||||
|
||||
def find_iter(self, **kwargs):
|
||||
"""Iterate over resources with pagination.
|
||||
|
||||
:key str org: The organization name.
|
||||
:key str org_id: The organization ID.
|
||||
:key str after: The last resource ID from which to seek from (but not including).
|
||||
:key int limit: the maximum number of items per page
|
||||
:return: resources iterator
|
||||
"""
|
||||
|
||||
def get_next_page(page: _Page):
|
||||
return self._find_next_page(page, **kwargs)
|
||||
|
||||
return iter(_PageIterator(_Page.initial(kwargs.get('after')), get_next_page))
|
||||
|
||||
def _find_next_page(self, page: _Page, **kwargs):
|
||||
if not page.has_next:
|
||||
return _Page.empty()
|
||||
|
||||
kw_args = {**kwargs, 'after': page.next_after} if page.next_after is not None else kwargs
|
||||
response = self.paginated_getter(**kw_args)
|
||||
|
||||
resources = self.pluck_page_resources_from_response(response)
|
||||
has_next = response.links.next is not None
|
||||
last_id = resources[-1].id if resources else None
|
||||
|
||||
return _Page(resources, has_next, last_id)
|
||||
@@ -0,0 +1,136 @@
|
||||
"""Authorization is about managing the security of your InfluxDB instance."""
|
||||
|
||||
from influxdb_client import Authorization, AuthorizationsService, User, Organization
|
||||
|
||||
|
||||
class AuthorizationsApi(object):
|
||||
"""Implementation for '/api/v2/authorizations' endpoint."""
|
||||
|
||||
def __init__(self, influxdb_client):
|
||||
"""Initialize defaults."""
|
||||
self._influxdb_client = influxdb_client
|
||||
self._authorizations_service = AuthorizationsService(influxdb_client.api_client)
|
||||
|
||||
def create_authorization(self, org_id: str = None, permissions: list = None,
|
||||
authorization: Authorization = None) -> Authorization:
|
||||
"""
|
||||
Create an authorization.
|
||||
|
||||
:type permissions: list of Permission
|
||||
:param org_id: organization id
|
||||
:param permissions: list of permissions
|
||||
:type authorization: authorization object
|
||||
|
||||
"""
|
||||
if authorization is not None:
|
||||
if not isinstance(authorization, Authorization):
|
||||
raise TypeError(f"Attempt to use non-Authorization value for authorization: {authorization}")
|
||||
return self._authorizations_service.post_authorizations(authorization_post_request=authorization)
|
||||
|
||||
# if org_id is not None and permissions is not None:
|
||||
authorization = Authorization(org_id=org_id, permissions=permissions)
|
||||
return self._authorizations_service.post_authorizations(authorization_post_request=authorization)
|
||||
|
||||
def find_authorization_by_id(self, auth_id: str) -> Authorization:
|
||||
"""
|
||||
Find authorization by id.
|
||||
|
||||
:param auth_id: authorization id
|
||||
:return: Authorization
|
||||
"""
|
||||
return self._authorizations_service.get_authorizations_id(auth_id=auth_id)
|
||||
|
||||
def find_authorizations(self, **kwargs):
|
||||
"""
|
||||
Get a list of all authorizations.
|
||||
|
||||
:key str user_id: filter authorizations belonging to a user id
|
||||
:key str user: filter authorizations belonging to a user name
|
||||
:key str org_id: filter authorizations belonging to a org id
|
||||
:key str org: filter authorizations belonging to a org name
|
||||
:return: Authorizations
|
||||
"""
|
||||
authorizations = self._authorizations_service.get_authorizations(**kwargs)
|
||||
|
||||
return authorizations.authorizations
|
||||
|
||||
def find_authorizations_by_user(self, user: User):
|
||||
"""
|
||||
Find authorization by User.
|
||||
|
||||
:return: Authorization list
|
||||
"""
|
||||
return self.find_authorizations(user_id=user.id)
|
||||
|
||||
def find_authorizations_by_user_id(self, user_id: str):
|
||||
"""
|
||||
Find authorization by user id.
|
||||
|
||||
:return: Authorization list
|
||||
"""
|
||||
return self.find_authorizations(user_id=user_id)
|
||||
|
||||
def find_authorizations_by_user_name(self, user_name: str):
|
||||
"""
|
||||
Find authorization by user name.
|
||||
|
||||
:return: Authorization list
|
||||
"""
|
||||
return self.find_authorizations(user=user_name)
|
||||
|
||||
def find_authorizations_by_org(self, org: Organization):
|
||||
"""
|
||||
Find authorization by user name.
|
||||
|
||||
:return: Authorization list
|
||||
"""
|
||||
if isinstance(org, Organization):
|
||||
return self.find_authorizations(org_id=org.id)
|
||||
|
||||
def find_authorizations_by_org_name(self, org_name: str):
|
||||
"""
|
||||
Find authorization by org name.
|
||||
|
||||
:return: Authorization list
|
||||
"""
|
||||
return self.find_authorizations(org=org_name)
|
||||
|
||||
def find_authorizations_by_org_id(self, org_id: str):
|
||||
"""
|
||||
Find authorization by org id.
|
||||
|
||||
:return: Authorization list
|
||||
"""
|
||||
return self.find_authorizations(org_id=org_id)
|
||||
|
||||
def update_authorization(self, auth):
|
||||
"""
|
||||
Update authorization object.
|
||||
|
||||
:param auth:
|
||||
:return:
|
||||
"""
|
||||
return self._authorizations_service.patch_authorizations_id(auth_id=auth.id, authorization_update_request=auth)
|
||||
|
||||
def clone_authorization(self, auth) -> Authorization:
|
||||
"""Clone an authorization."""
|
||||
if isinstance(auth, Authorization):
|
||||
cloned = Authorization(org_id=auth.org_id, permissions=auth.permissions)
|
||||
# cloned.description = auth.description
|
||||
# cloned.status = auth.status
|
||||
return self.create_authorization(authorization=cloned)
|
||||
|
||||
if isinstance(auth, str):
|
||||
authorization = self.find_authorization_by_id(auth)
|
||||
return self.clone_authorization(auth=authorization)
|
||||
|
||||
raise ValueError("Invalid argument")
|
||||
|
||||
def delete_authorization(self, auth):
|
||||
"""Delete a authorization."""
|
||||
if isinstance(auth, Authorization):
|
||||
return self._authorizations_service.delete_authorizations_id(auth_id=auth.id)
|
||||
|
||||
if isinstance(auth, str):
|
||||
return self._authorizations_service.delete_authorizations_id(auth_id=auth)
|
||||
raise ValueError("Invalid argument")
|
||||
@@ -0,0 +1,132 @@
|
||||
"""
|
||||
A bucket is a named location where time series data is stored.
|
||||
|
||||
All buckets have a retention policy, a duration of time that each data point persists.
|
||||
A bucket belongs to an organization.
|
||||
"""
|
||||
import warnings
|
||||
|
||||
from influxdb_client import BucketsService, Bucket, PostBucketRequest, PatchBucketRequest
|
||||
from influxdb_client.client.util.helpers import get_org_query_param
|
||||
from influxdb_client.client._pages import _Paginated
|
||||
|
||||
|
||||
class BucketsApi(object):
|
||||
"""Implementation for '/api/v2/buckets' endpoint."""
|
||||
|
||||
def __init__(self, influxdb_client):
|
||||
"""Initialize defaults."""
|
||||
self._influxdb_client = influxdb_client
|
||||
self._buckets_service = BucketsService(influxdb_client.api_client)
|
||||
|
||||
def create_bucket(self, bucket=None, bucket_name=None, org_id=None, retention_rules=None,
|
||||
description=None, org=None) -> Bucket:
|
||||
"""Create a bucket.
|
||||
|
||||
:param Bucket|PostBucketRequest bucket: bucket to create
|
||||
:param bucket_name: bucket name
|
||||
:param description: bucket description
|
||||
:param org_id: org_id
|
||||
:param bucket_name: bucket name
|
||||
:param retention_rules: retention rules array or single BucketRetentionRules
|
||||
:param str, Organization org: specifies the organization for create the bucket;
|
||||
Take the ``ID``, ``Name`` or ``Organization``.
|
||||
If not specified the default value from ``InfluxDBClient.org`` is used.
|
||||
:return: Bucket
|
||||
If the method is called asynchronously,
|
||||
returns the request thread.
|
||||
"""
|
||||
if retention_rules is None:
|
||||
retention_rules = []
|
||||
|
||||
rules = []
|
||||
|
||||
if isinstance(retention_rules, list):
|
||||
rules.extend(retention_rules)
|
||||
else:
|
||||
rules.append(retention_rules)
|
||||
|
||||
if org_id is not None:
|
||||
warnings.warn("org_id is deprecated; use org", DeprecationWarning)
|
||||
|
||||
if bucket is None:
|
||||
bucket = PostBucketRequest(name=bucket_name,
|
||||
retention_rules=rules,
|
||||
description=description,
|
||||
org_id=get_org_query_param(org=(org_id if org is None else org),
|
||||
client=self._influxdb_client,
|
||||
required_id=True))
|
||||
|
||||
return self._buckets_service.post_buckets(post_bucket_request=bucket)
|
||||
|
||||
def update_bucket(self, bucket: Bucket) -> Bucket:
|
||||
"""Update a bucket.
|
||||
|
||||
:param bucket: Bucket update to apply (required)
|
||||
:return: Bucket
|
||||
"""
|
||||
request = PatchBucketRequest(name=bucket.name,
|
||||
description=bucket.description,
|
||||
retention_rules=bucket.retention_rules)
|
||||
|
||||
return self._buckets_service.patch_buckets_id(bucket_id=bucket.id, patch_bucket_request=request)
|
||||
|
||||
def delete_bucket(self, bucket):
|
||||
"""Delete a bucket.
|
||||
|
||||
:param bucket: bucket id or Bucket
|
||||
:return: Bucket
|
||||
"""
|
||||
if isinstance(bucket, Bucket):
|
||||
bucket_id = bucket.id
|
||||
else:
|
||||
bucket_id = bucket
|
||||
|
||||
return self._buckets_service.delete_buckets_id(bucket_id=bucket_id)
|
||||
|
||||
def find_bucket_by_id(self, id):
|
||||
"""Find bucket by ID.
|
||||
|
||||
:param id:
|
||||
:return:
|
||||
"""
|
||||
return self._buckets_service.get_buckets_id(id)
|
||||
|
||||
def find_bucket_by_name(self, bucket_name):
|
||||
"""Find bucket by name.
|
||||
|
||||
:param bucket_name: bucket name
|
||||
:return: Bucket
|
||||
"""
|
||||
buckets = self._buckets_service.get_buckets(name=bucket_name)
|
||||
|
||||
if len(buckets.buckets) > 0:
|
||||
return buckets.buckets[0]
|
||||
else:
|
||||
return None
|
||||
|
||||
def find_buckets(self, **kwargs):
|
||||
"""List buckets.
|
||||
|
||||
:key int offset: Offset for pagination
|
||||
:key int limit: Limit for pagination
|
||||
:key str after: The last resource ID from which to seek from (but not including).
|
||||
This is to be used instead of `offset`.
|
||||
:key str org: The organization name.
|
||||
:key str org_id: The organization ID.
|
||||
:key str name: Only returns buckets with a specific name.
|
||||
:return: Buckets
|
||||
"""
|
||||
return self._buckets_service.get_buckets(**kwargs)
|
||||
|
||||
def find_buckets_iter(self, **kwargs):
|
||||
"""Iterate over all buckets with pagination.
|
||||
|
||||
:key str name: Only returns buckets with the specified name
|
||||
:key str org: The organization name.
|
||||
:key str org_id: The organization ID.
|
||||
:key str after: The last resource ID from which to seek from (but not including).
|
||||
:key int limit: the maximum number of buckets in one page
|
||||
:return: Buckets iterator
|
||||
"""
|
||||
return _Paginated(self._buckets_service.get_buckets, lambda response: response.buckets).find_iter(**kwargs)
|
||||
@@ -0,0 +1,35 @@
|
||||
"""Delete time series data from InfluxDB."""
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Union
|
||||
|
||||
from influxdb_client import Organization
|
||||
from influxdb_client.client._base import _BaseDeleteApi
|
||||
from influxdb_client.client.util.helpers import get_org_query_param
|
||||
|
||||
|
||||
class DeleteApi(_BaseDeleteApi):
|
||||
"""Implementation for '/api/v2/delete' endpoint."""
|
||||
|
||||
def __init__(self, influxdb_client):
|
||||
"""Initialize defaults."""
|
||||
super().__init__(influxdb_client)
|
||||
|
||||
def delete(self, start: Union[str, datetime], stop: Union[str, datetime], predicate: str, bucket: str,
|
||||
org: Union[str, Organization, None] = None) -> None:
|
||||
"""
|
||||
Delete Time series data from InfluxDB.
|
||||
|
||||
:param str, datetime.datetime start: start time
|
||||
:param str, datetime.datetime stop: stop time
|
||||
:param str predicate: predicate
|
||||
:param str bucket: bucket id or name from which data will be deleted
|
||||
:param str, Organization org: specifies the organization to delete data from.
|
||||
Take the ``ID``, ``Name`` or ``Organization``.
|
||||
If not specified the default value from ``InfluxDBClient.org`` is used.
|
||||
:return:
|
||||
"""
|
||||
predicate_request = self._prepare_predicate_request(start, stop, predicate)
|
||||
org_param = get_org_query_param(org=org, client=self._influxdb_client, required_id=False)
|
||||
|
||||
return self._service.post_delete(delete_predicate_request=predicate_request, bucket=bucket, org=org_param)
|
||||
@@ -0,0 +1,37 @@
|
||||
"""Delete time series data from InfluxDB."""
|
||||
|
||||
from datetime import datetime
|
||||
from typing import Union
|
||||
|
||||
from influxdb_client import Organization
|
||||
from influxdb_client.client._base import _BaseDeleteApi
|
||||
from influxdb_client.client.util.helpers import get_org_query_param
|
||||
|
||||
|
||||
class DeleteApiAsync(_BaseDeleteApi):
|
||||
"""Async implementation for '/api/v2/delete' endpoint."""
|
||||
|
||||
def __init__(self, influxdb_client):
|
||||
"""Initialize defaults."""
|
||||
super().__init__(influxdb_client)
|
||||
|
||||
async def delete(self, start: Union[str, datetime], stop: Union[str, datetime], predicate: str, bucket: str,
|
||||
org: Union[str, Organization, None] = None) -> bool:
|
||||
"""
|
||||
Delete Time series data from InfluxDB.
|
||||
|
||||
:param str, datetime.datetime start: start time
|
||||
:param str, datetime.datetime stop: stop time
|
||||
:param str predicate: predicate
|
||||
:param str bucket: bucket id or name from which data will be deleted
|
||||
:param str, Organization org: specifies the organization to delete data from.
|
||||
Take the ``ID``, ``Name`` or ``Organization``.
|
||||
If not specified the default value from ``InfluxDBClientAsync.org`` is used.
|
||||
:return: ``True`` for successfully deleted data, otherwise raise an exception
|
||||
"""
|
||||
predicate_request = self._prepare_predicate_request(start, stop, predicate)
|
||||
org_param = get_org_query_param(org=org, client=self._influxdb_client, required_id=False)
|
||||
|
||||
response = await self._service.post_delete_async(delete_predicate_request=predicate_request, bucket=bucket,
|
||||
org=org_param, _return_http_data_only=False)
|
||||
return response[1] == 204
|
||||