forked from home-assistant/core
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d8df89a34e |
@@ -1,374 +0,0 @@
|
||||
name: Auto-detect duplicate issues
|
||||
|
||||
# yamllint disable-line rule:truthy
|
||||
on:
|
||||
issues:
|
||||
types: [labeled]
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
models: read
|
||||
|
||||
jobs:
|
||||
detect-duplicates:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Check if integration label was added and extract details
|
||||
id: extract
|
||||
uses: actions/github-script@v7.0.1
|
||||
with:
|
||||
script: |
|
||||
// Debug: Log the event payload
|
||||
console.log('Event name:', context.eventName);
|
||||
console.log('Event action:', context.payload.action);
|
||||
console.log('Event payload keys:', Object.keys(context.payload));
|
||||
|
||||
// Check the specific label that was added
|
||||
const addedLabel = context.payload.label;
|
||||
if (!addedLabel) {
|
||||
console.log('No label found in labeled event payload');
|
||||
core.setOutput('should_continue', 'false');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`Label added: ${addedLabel.name}`);
|
||||
|
||||
if (!addedLabel.name.startsWith('integration:')) {
|
||||
console.log('Added label is not an integration label, skipping duplicate detection');
|
||||
core.setOutput('should_continue', 'false');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`Integration label added: ${addedLabel.name}`);
|
||||
|
||||
let currentIssue;
|
||||
let integrationLabels = [];
|
||||
|
||||
try {
|
||||
const issue = await github.rest.issues.get({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.payload.issue.number
|
||||
});
|
||||
|
||||
currentIssue = issue.data;
|
||||
|
||||
// Check if potential-duplicate label already exists
|
||||
const hasPotentialDuplicateLabel = currentIssue.labels
|
||||
.some(label => label.name === 'potential-duplicate');
|
||||
|
||||
if (hasPotentialDuplicateLabel) {
|
||||
console.log('Issue already has potential-duplicate label, skipping duplicate detection');
|
||||
core.setOutput('should_continue', 'false');
|
||||
return;
|
||||
}
|
||||
|
||||
integrationLabels = currentIssue.labels
|
||||
.filter(label => label.name.startsWith('integration:'))
|
||||
.map(label => label.name);
|
||||
} catch (error) {
|
||||
core.error(`Failed to fetch issue #${context.payload.issue.number}:`, error.message);
|
||||
core.setOutput('should_continue', 'false');
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if we've already posted a duplicate detection comment recently
|
||||
let comments;
|
||||
try {
|
||||
comments = await github.rest.issues.listComments({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.payload.issue.number,
|
||||
per_page: 10
|
||||
});
|
||||
} catch (error) {
|
||||
core.error('Failed to fetch comments:', error.message);
|
||||
// Continue anyway, worst case we might post a duplicate comment
|
||||
comments = { data: [] };
|
||||
}
|
||||
|
||||
// Check if we've already posted a duplicate detection comment
|
||||
const recentDuplicateComment = comments.data.find(comment =>
|
||||
comment.user && comment.user.login === 'github-actions[bot]' &&
|
||||
comment.body.includes('<!-- workflow: detect-duplicate-issues -->')
|
||||
);
|
||||
|
||||
if (recentDuplicateComment) {
|
||||
console.log('Already posted duplicate detection comment, skipping');
|
||||
core.setOutput('should_continue', 'false');
|
||||
return;
|
||||
}
|
||||
|
||||
core.setOutput('should_continue', 'true');
|
||||
core.setOutput('current_number', currentIssue.number);
|
||||
core.setOutput('current_title', currentIssue.title);
|
||||
core.setOutput('current_body', currentIssue.body);
|
||||
core.setOutput('current_url', currentIssue.html_url);
|
||||
core.setOutput('integration_labels', JSON.stringify(integrationLabels));
|
||||
|
||||
console.log(`Current issue: #${currentIssue.number}`);
|
||||
console.log(`Integration labels: ${integrationLabels.join(', ')}`);
|
||||
|
||||
- name: Fetch similar issues
|
||||
id: fetch_similar
|
||||
if: steps.extract.outputs.should_continue == 'true'
|
||||
uses: actions/github-script@v7.0.1
|
||||
env:
|
||||
INTEGRATION_LABELS: ${{ steps.extract.outputs.integration_labels }}
|
||||
CURRENT_NUMBER: ${{ steps.extract.outputs.current_number }}
|
||||
with:
|
||||
script: |
|
||||
const integrationLabels = JSON.parse(process.env.INTEGRATION_LABELS);
|
||||
const currentNumber = parseInt(process.env.CURRENT_NUMBER);
|
||||
|
||||
if (integrationLabels.length === 0) {
|
||||
console.log('No integration labels found, skipping duplicate detection');
|
||||
core.setOutput('has_similar', 'false');
|
||||
return;
|
||||
}
|
||||
|
||||
// Use GitHub search API to find issues with matching integration labels
|
||||
console.log(`Searching for issues with integration labels: ${integrationLabels.join(', ')}`);
|
||||
|
||||
// Build search query for issues with any of the current integration labels
|
||||
const labelQueries = integrationLabels.map(label => `label:"${label}"`);
|
||||
let searchQuery;
|
||||
|
||||
if (labelQueries.length === 1) {
|
||||
searchQuery = `repo:${context.repo.owner}/${context.repo.repo} is:issue ${labelQueries[0]}`;
|
||||
} else {
|
||||
searchQuery = `repo:${context.repo.owner}/${context.repo.repo} is:issue (${labelQueries.join(' OR ')})`;
|
||||
}
|
||||
|
||||
console.log(`Search query: ${searchQuery}`);
|
||||
|
||||
let result;
|
||||
try {
|
||||
result = await github.rest.search.issuesAndPullRequests({
|
||||
q: searchQuery,
|
||||
per_page: 15,
|
||||
sort: 'updated',
|
||||
order: 'desc'
|
||||
});
|
||||
} catch (error) {
|
||||
core.error('Failed to search for similar issues:', error.message);
|
||||
if (error.status === 403 && error.message.includes('rate limit')) {
|
||||
core.error('GitHub API rate limit exceeded');
|
||||
}
|
||||
core.setOutput('has_similar', 'false');
|
||||
return;
|
||||
}
|
||||
|
||||
// Filter out the current issue, pull requests, and newer issues (higher numbers)
|
||||
const similarIssues = result.data.items
|
||||
.filter(item =>
|
||||
item.number !== currentNumber &&
|
||||
!item.pull_request &&
|
||||
item.number < currentNumber // Only include older issues (lower numbers)
|
||||
)
|
||||
.map(item => ({
|
||||
number: item.number,
|
||||
title: item.title,
|
||||
body: item.body,
|
||||
url: item.html_url,
|
||||
state: item.state,
|
||||
createdAt: item.created_at,
|
||||
updatedAt: item.updated_at,
|
||||
comments: item.comments,
|
||||
labels: item.labels.map(l => l.name)
|
||||
}));
|
||||
|
||||
console.log(`Found ${similarIssues.length} issues with matching integration labels`);
|
||||
console.log('Raw similar issues:', JSON.stringify(similarIssues.slice(0, 3), null, 2));
|
||||
|
||||
if (similarIssues.length === 0) {
|
||||
console.log('No similar issues found, setting has_similar to false');
|
||||
core.setOutput('has_similar', 'false');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('Similar issues found, setting has_similar to true');
|
||||
core.setOutput('has_similar', 'true');
|
||||
|
||||
// Clean the issue data to prevent JSON parsing issues
|
||||
const cleanedIssues = similarIssues.slice(0, 15).map(item => {
|
||||
// Handle body with improved truncation and null handling
|
||||
let cleanBody = '';
|
||||
if (item.body && typeof item.body === 'string') {
|
||||
// Remove control characters
|
||||
const cleaned = item.body.replace(/[\u0000-\u001F\u007F-\u009F]/g, '');
|
||||
// Truncate to 1000 characters and add ellipsis if needed
|
||||
cleanBody = cleaned.length > 1000
|
||||
? cleaned.substring(0, 1000) + '...'
|
||||
: cleaned;
|
||||
}
|
||||
|
||||
return {
|
||||
number: item.number,
|
||||
title: item.title.replace(/[\u0000-\u001F\u007F-\u009F]/g, ''), // Remove control characters
|
||||
body: cleanBody,
|
||||
url: item.url,
|
||||
state: item.state,
|
||||
createdAt: item.createdAt,
|
||||
updatedAt: item.updatedAt,
|
||||
comments: item.comments,
|
||||
labels: item.labels
|
||||
};
|
||||
});
|
||||
|
||||
console.log(`Cleaned issues count: ${cleanedIssues.length}`);
|
||||
console.log('First cleaned issue:', JSON.stringify(cleanedIssues[0], null, 2));
|
||||
|
||||
core.setOutput('similar_issues', JSON.stringify(cleanedIssues));
|
||||
|
||||
- name: Detect duplicates using AI
|
||||
id: ai_detection
|
||||
if: steps.extract.outputs.should_continue == 'true' && steps.fetch_similar.outputs.has_similar == 'true'
|
||||
uses: actions/ai-inference@v1.1.0
|
||||
with:
|
||||
model: openai/gpt-4o-mini
|
||||
system-prompt: |
|
||||
You are a Home Assistant issue duplicate detector. Your task is to identify potential duplicate issues based on their content.
|
||||
|
||||
Important considerations:
|
||||
- Open issues are more relevant than closed ones for duplicate detection
|
||||
- Recently updated issues may indicate ongoing work or discussion
|
||||
- Issues with more comments are generally more relevant and active
|
||||
- Higher comment count often indicates community engagement and importance
|
||||
- Older closed issues might be resolved differently than newer approaches
|
||||
- Consider the time between issues - very old issues may have different contexts
|
||||
|
||||
Rules:
|
||||
1. Compare the current issue with the provided similar issues
|
||||
2. Look for issues that report the same problem or request the same functionality
|
||||
3. Consider different wording but same underlying issue as duplicates
|
||||
4. For CLOSED issues, only mark as duplicate if they describe the EXACT same problem
|
||||
5. For OPEN issues, use a lower threshold (70%+ similarity)
|
||||
6. Prioritize issues with higher comment counts as they indicate more activity/relevance
|
||||
7. Return ONLY a JSON array of issue numbers that are potential duplicates
|
||||
8. If no duplicates are found, return an empty array: []
|
||||
9. Maximum 5 potential duplicates, prioritize open issues with comments
|
||||
10. Consider the age of issues - prefer recent duplicates over very old ones
|
||||
|
||||
Example response format:
|
||||
[1234, 5678, 9012]
|
||||
|
||||
prompt: |
|
||||
Current issue (just created):
|
||||
Title: ${{ steps.extract.outputs.current_title }}
|
||||
Body: ${{ steps.extract.outputs.current_body }}
|
||||
|
||||
Similar issues to compare against (each includes state, creation date, last update, and comment count):
|
||||
${{ steps.fetch_similar.outputs.similar_issues }}
|
||||
|
||||
Analyze these issues and identify which ones are potential duplicates of the current issue. Consider their state (open/closed), how recently they were updated, and their comment count (higher = more relevant).
|
||||
|
||||
max-tokens: 100
|
||||
|
||||
- name: Post duplicate detection results
|
||||
id: post_results
|
||||
if: steps.extract.outputs.should_continue == 'true' && steps.fetch_similar.outputs.has_similar == 'true'
|
||||
uses: actions/github-script@v7.0.1
|
||||
env:
|
||||
AI_RESPONSE: ${{ steps.ai_detection.outputs.response }}
|
||||
SIMILAR_ISSUES: ${{ steps.fetch_similar.outputs.similar_issues }}
|
||||
with:
|
||||
script: |
|
||||
const aiResponse = process.env.AI_RESPONSE;
|
||||
|
||||
console.log('Raw AI response:', JSON.stringify(aiResponse));
|
||||
|
||||
let duplicateNumbers = [];
|
||||
try {
|
||||
// Clean the response of any potential control characters
|
||||
const cleanResponse = aiResponse.trim().replace(/[\u0000-\u001F\u007F-\u009F]/g, '');
|
||||
console.log('Cleaned AI response:', cleanResponse);
|
||||
|
||||
duplicateNumbers = JSON.parse(cleanResponse);
|
||||
|
||||
// Ensure it's an array and contains only numbers
|
||||
if (!Array.isArray(duplicateNumbers)) {
|
||||
console.log('AI response is not an array, trying to extract numbers');
|
||||
const numberMatches = cleanResponse.match(/\d+/g);
|
||||
duplicateNumbers = numberMatches ? numberMatches.map(n => parseInt(n)) : [];
|
||||
}
|
||||
|
||||
// Filter to only valid numbers
|
||||
duplicateNumbers = duplicateNumbers.filter(n => typeof n === 'number' && !isNaN(n));
|
||||
|
||||
} catch (error) {
|
||||
console.log('Failed to parse AI response as JSON:', error.message);
|
||||
console.log('Raw response:', aiResponse);
|
||||
|
||||
// Fallback: try to extract numbers from the response
|
||||
const numberMatches = aiResponse.match(/\d+/g);
|
||||
duplicateNumbers = numberMatches ? numberMatches.map(n => parseInt(n)) : [];
|
||||
console.log('Extracted numbers as fallback:', duplicateNumbers);
|
||||
}
|
||||
|
||||
if (!Array.isArray(duplicateNumbers) || duplicateNumbers.length === 0) {
|
||||
console.log('No duplicates detected by AI');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`AI detected ${duplicateNumbers.length} potential duplicates: ${duplicateNumbers.join(', ')}`);
|
||||
|
||||
// Get details of detected duplicates
|
||||
const similarIssues = JSON.parse(process.env.SIMILAR_ISSUES);
|
||||
const duplicates = similarIssues.filter(issue => duplicateNumbers.includes(issue.number));
|
||||
|
||||
if (duplicates.length === 0) {
|
||||
console.log('No matching issues found for detected numbers');
|
||||
return;
|
||||
}
|
||||
|
||||
// Create comment with duplicate detection results
|
||||
const duplicateLinks = duplicates.map(issue => `- [#${issue.number}: ${issue.title}](${issue.url})`).join('\n');
|
||||
|
||||
const commentBody = [
|
||||
'<!-- workflow: detect-duplicate-issues -->',
|
||||
'### 🔍 **Potential duplicate detection**',
|
||||
'',
|
||||
'I\'ve analyzed similar issues and found the following potential duplicates:',
|
||||
'',
|
||||
duplicateLinks,
|
||||
'',
|
||||
'**What to do next:**',
|
||||
'1. Please review these issues to see if they match your issue',
|
||||
'2. If you find an existing issue that covers your problem:',
|
||||
' - Consider closing this issue',
|
||||
' - Add your findings or 👍 on the existing issue instead',
|
||||
'3. If your issue is different or adds new aspects, please clarify how it differs',
|
||||
'',
|
||||
'This helps keep our issues organized and ensures similar issues are consolidated for better visibility.',
|
||||
'',
|
||||
'*This message was generated automatically by our duplicate detection system.*'
|
||||
].join('\n');
|
||||
|
||||
try {
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.payload.issue.number,
|
||||
body: commentBody
|
||||
});
|
||||
|
||||
console.log(`Posted duplicate detection comment with ${duplicates.length} potential duplicates`);
|
||||
|
||||
// Add the potential-duplicate label
|
||||
await github.rest.issues.addLabels({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: context.payload.issue.number,
|
||||
labels: ['potential-duplicate']
|
||||
});
|
||||
|
||||
console.log('Added potential-duplicate label to the issue');
|
||||
} catch (error) {
|
||||
core.error('Failed to post duplicate detection comment or add label:', error.message);
|
||||
if (error.status === 403) {
|
||||
core.error('Permission denied or rate limit exceeded');
|
||||
}
|
||||
// Don't throw - we've done the analysis, just couldn't post the result
|
||||
}
|
||||
@@ -1,184 +0,0 @@
|
||||
name: Auto-detect non-English issues
|
||||
|
||||
# yamllint disable-line rule:truthy
|
||||
on:
|
||||
issues:
|
||||
types: [opened]
|
||||
|
||||
permissions:
|
||||
issues: write
|
||||
models: read
|
||||
|
||||
jobs:
|
||||
detect-language:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Check issue language
|
||||
id: detect_language
|
||||
uses: actions/github-script@v7.0.1
|
||||
env:
|
||||
ISSUE_NUMBER: ${{ github.event.issue.number }}
|
||||
ISSUE_TITLE: ${{ github.event.issue.title }}
|
||||
ISSUE_BODY: ${{ github.event.issue.body }}
|
||||
ISSUE_USER_TYPE: ${{ github.event.issue.user.type }}
|
||||
with:
|
||||
script: |
|
||||
// Get the issue details from environment variables
|
||||
const issueNumber = process.env.ISSUE_NUMBER;
|
||||
const issueTitle = process.env.ISSUE_TITLE || '';
|
||||
const issueBody = process.env.ISSUE_BODY || '';
|
||||
const userType = process.env.ISSUE_USER_TYPE;
|
||||
|
||||
// Skip language detection for bot users
|
||||
if (userType === 'Bot') {
|
||||
console.log('Skipping language detection for bot user');
|
||||
core.setOutput('should_continue', 'false');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`Checking language for issue #${issueNumber}`);
|
||||
console.log(`Title: ${issueTitle}`);
|
||||
|
||||
// Combine title and body for language detection
|
||||
const fullText = `${issueTitle}\n\n${issueBody}`;
|
||||
|
||||
// Check if the text is too short to reliably detect language
|
||||
if (fullText.trim().length < 20) {
|
||||
console.log('Text too short for reliable language detection');
|
||||
core.setOutput('should_continue', 'false'); // Skip processing for very short text
|
||||
return;
|
||||
}
|
||||
|
||||
core.setOutput('issue_number', issueNumber);
|
||||
core.setOutput('issue_text', fullText);
|
||||
core.setOutput('should_continue', 'true');
|
||||
|
||||
- name: Detect language using AI
|
||||
id: ai_language_detection
|
||||
if: steps.detect_language.outputs.should_continue == 'true'
|
||||
uses: actions/ai-inference@v1.1.0
|
||||
with:
|
||||
model: openai/gpt-4o-mini
|
||||
system-prompt: |
|
||||
You are a language detection system. Your task is to determine if the provided text is written in English or another language.
|
||||
|
||||
Rules:
|
||||
1. Analyze the text and determine the primary language
|
||||
2. IGNORE markdown headers (lines starting with #, ##, ###, etc.) as these are from issue templates, not user input
|
||||
3. IGNORE all code blocks (text between ``` or ` markers) as they may contain system-generated error messages in other languages
|
||||
4. Consider technical terms, code snippets, and URLs as neutral (they don't indicate non-English)
|
||||
5. Focus on the actual sentences and descriptions written by the user
|
||||
6. Return ONLY a JSON object with two fields:
|
||||
- "is_english": boolean (true if the text is primarily in English, false otherwise)
|
||||
- "detected_language": string (the name of the detected language, e.g., "English", "Spanish", "Chinese", etc.)
|
||||
7. Be lenient - if the text is mostly English with minor non-English elements, consider it English
|
||||
8. Common programming terms, error messages, and technical jargon should not be considered as non-English
|
||||
|
||||
Example response:
|
||||
{"is_english": false, "detected_language": "Spanish"}
|
||||
|
||||
prompt: |
|
||||
Please analyze the following issue text and determine if it is written in English:
|
||||
|
||||
${{ steps.detect_language.outputs.issue_text }}
|
||||
|
||||
max-tokens: 50
|
||||
|
||||
- name: Process non-English issues
|
||||
if: steps.detect_language.outputs.should_continue == 'true'
|
||||
uses: actions/github-script@v7.0.1
|
||||
env:
|
||||
AI_RESPONSE: ${{ steps.ai_language_detection.outputs.response }}
|
||||
ISSUE_NUMBER: ${{ steps.detect_language.outputs.issue_number }}
|
||||
with:
|
||||
script: |
|
||||
const issueNumber = parseInt(process.env.ISSUE_NUMBER);
|
||||
const aiResponse = process.env.AI_RESPONSE;
|
||||
|
||||
console.log('AI language detection response:', aiResponse);
|
||||
|
||||
let languageResult;
|
||||
try {
|
||||
languageResult = JSON.parse(aiResponse.trim());
|
||||
|
||||
// Validate the response structure
|
||||
if (!languageResult || typeof languageResult.is_english !== 'boolean') {
|
||||
throw new Error('Invalid response structure');
|
||||
}
|
||||
} catch (error) {
|
||||
core.error(`Failed to parse AI response: ${error.message}`);
|
||||
console.log('Raw AI response:', aiResponse);
|
||||
|
||||
// Log more details for debugging
|
||||
core.warning('Defaulting to English due to parsing error');
|
||||
|
||||
// Default to English if we can't parse the response
|
||||
return;
|
||||
}
|
||||
|
||||
if (languageResult.is_english) {
|
||||
console.log('Issue is in English, no action needed');
|
||||
return;
|
||||
}
|
||||
|
||||
console.log(`Issue detected as non-English: ${languageResult.detected_language}`);
|
||||
|
||||
// Post comment explaining the language requirement
|
||||
const commentBody = [
|
||||
'<!-- workflow: detect-non-english-issues -->',
|
||||
'### 🌐 Non-English issue detected',
|
||||
'',
|
||||
`This issue appears to be written in **${languageResult.detected_language}** rather than English.`,
|
||||
'',
|
||||
'The Home Assistant project uses English as the primary language for issues to ensure that everyone in our international community can participate and help resolve issues. This allows any of our thousands of contributors to jump in and provide assistance.',
|
||||
'',
|
||||
'**What to do:**',
|
||||
'1. Re-create the issue using the English language',
|
||||
'2. If you need help with translation, consider using:',
|
||||
' - Translation tools like Google Translate',
|
||||
' - AI assistants like ChatGPT or Claude',
|
||||
'',
|
||||
'This helps our community provide the best possible support and ensures your issue gets the attention it deserves from our global contributor base.',
|
||||
'',
|
||||
'Thank you for your understanding! 🙏'
|
||||
].join('\n');
|
||||
|
||||
try {
|
||||
// Add comment
|
||||
await github.rest.issues.createComment({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issueNumber,
|
||||
body: commentBody
|
||||
});
|
||||
|
||||
console.log('Posted language requirement comment');
|
||||
|
||||
// Add non-english label
|
||||
await github.rest.issues.addLabels({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issueNumber,
|
||||
labels: ['non-english']
|
||||
});
|
||||
|
||||
console.log('Added non-english label');
|
||||
|
||||
// Close the issue
|
||||
await github.rest.issues.update({
|
||||
owner: context.repo.owner,
|
||||
repo: context.repo.repo,
|
||||
issue_number: issueNumber,
|
||||
state: 'closed',
|
||||
state_reason: 'not_planned'
|
||||
});
|
||||
|
||||
console.log('Closed the issue');
|
||||
|
||||
} catch (error) {
|
||||
core.error('Failed to process non-English issue:', error.message);
|
||||
if (error.status === 403) {
|
||||
core.error('Permission denied or rate limit exceeded');
|
||||
}
|
||||
}
|
||||
+1
-1
@@ -65,8 +65,8 @@ homeassistant.components.aladdin_connect.*
|
||||
homeassistant.components.alarm_control_panel.*
|
||||
homeassistant.components.alert.*
|
||||
homeassistant.components.alexa.*
|
||||
homeassistant.components.alexa_devices.*
|
||||
homeassistant.components.alpha_vantage.*
|
||||
homeassistant.components.amazon_devices.*
|
||||
homeassistant.components.amazon_polly.*
|
||||
homeassistant.components.amberelectric.*
|
||||
homeassistant.components.ambient_network.*
|
||||
|
||||
Generated
+2
-2
@@ -89,8 +89,8 @@ build.json @home-assistant/supervisor
|
||||
/tests/components/alert/ @home-assistant/core @frenck
|
||||
/homeassistant/components/alexa/ @home-assistant/cloud @ochlocracy @jbouwh
|
||||
/tests/components/alexa/ @home-assistant/cloud @ochlocracy @jbouwh
|
||||
/homeassistant/components/alexa_devices/ @chemelli74
|
||||
/tests/components/alexa_devices/ @chemelli74
|
||||
/homeassistant/components/amazon_devices/ @chemelli74
|
||||
/tests/components/amazon_devices/ @chemelli74
|
||||
/homeassistant/components/amazon_polly/ @jschlyter
|
||||
/homeassistant/components/amberelectric/ @madpilot
|
||||
/tests/components/amberelectric/ @madpilot
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
"""Enum backports from standard lib.
|
||||
|
||||
This file contained the backport of the StrEnum of Python 3.11.
|
||||
|
||||
Since we have dropped support for Python 3.10, we can remove this backport.
|
||||
This file is kept for now to avoid breaking custom components that might
|
||||
import it.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from enum import StrEnum as _StrEnum
|
||||
from functools import partial
|
||||
|
||||
from homeassistant.helpers.deprecation import (
|
||||
DeprecatedAlias,
|
||||
all_with_deprecated_constants,
|
||||
check_if_deprecated_constant,
|
||||
dir_with_deprecated_constants,
|
||||
)
|
||||
|
||||
# StrEnum deprecated as of 2024.5 use enum.StrEnum instead.
|
||||
_DEPRECATED_StrEnum = DeprecatedAlias(_StrEnum, "enum.StrEnum", "2025.5")
|
||||
|
||||
__getattr__ = partial(check_if_deprecated_constant, module_globals=globals())
|
||||
__dir__ = partial(
|
||||
dir_with_deprecated_constants, module_globals_keys=[*globals().keys()]
|
||||
)
|
||||
__all__ = all_with_deprecated_constants(globals())
|
||||
@@ -0,0 +1,31 @@
|
||||
"""Functools backports from standard lib.
|
||||
|
||||
This file contained the backport of the cached_property implementation of Python 3.12.
|
||||
|
||||
Since we have dropped support for Python 3.11, we can remove this backport.
|
||||
This file is kept for now to avoid breaking custom components that might
|
||||
import it.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
# pylint: disable-next=hass-deprecated-import
|
||||
from functools import cached_property as _cached_property, partial
|
||||
|
||||
from homeassistant.helpers.deprecation import (
|
||||
DeprecatedAlias,
|
||||
all_with_deprecated_constants,
|
||||
check_if_deprecated_constant,
|
||||
dir_with_deprecated_constants,
|
||||
)
|
||||
|
||||
# cached_property deprecated as of 2024.5 use functools.cached_property instead.
|
||||
_DEPRECATED_cached_property = DeprecatedAlias(
|
||||
_cached_property, "functools.cached_property", "2025.5"
|
||||
)
|
||||
|
||||
__getattr__ = partial(check_if_deprecated_constant, module_globals=globals())
|
||||
__dir__ = partial(
|
||||
dir_with_deprecated_constants, module_globals_keys=[*globals().keys()]
|
||||
)
|
||||
__all__ = all_with_deprecated_constants(globals())
|
||||
@@ -3,7 +3,7 @@
|
||||
"name": "Amazon",
|
||||
"integrations": [
|
||||
"alexa",
|
||||
"alexa_devices",
|
||||
"amazon_devices",
|
||||
"amazon_polly",
|
||||
"aws",
|
||||
"aws_s3",
|
||||
|
||||
@@ -37,35 +37,30 @@ SENSORS: dict[str, SensorEntityDescription] = {
|
||||
key="radonShortTermAvg",
|
||||
native_unit_of_measurement="Bq/m³",
|
||||
translation_key="radon",
|
||||
suggested_display_precision=0,
|
||||
),
|
||||
"temp": SensorEntityDescription(
|
||||
key="temp",
|
||||
device_class=SensorDeviceClass.TEMPERATURE,
|
||||
native_unit_of_measurement=UnitOfTemperature.CELSIUS,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
suggested_display_precision=1,
|
||||
),
|
||||
"humidity": SensorEntityDescription(
|
||||
key="humidity",
|
||||
device_class=SensorDeviceClass.HUMIDITY,
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
suggested_display_precision=0,
|
||||
),
|
||||
"pressure": SensorEntityDescription(
|
||||
key="pressure",
|
||||
device_class=SensorDeviceClass.ATMOSPHERIC_PRESSURE,
|
||||
native_unit_of_measurement=UnitOfPressure.MBAR,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
suggested_display_precision=1,
|
||||
),
|
||||
"sla": SensorEntityDescription(
|
||||
key="sla",
|
||||
device_class=SensorDeviceClass.SOUND_PRESSURE,
|
||||
native_unit_of_measurement=UnitOfSoundPressure.WEIGHTED_DECIBEL_A,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
suggested_display_precision=0,
|
||||
),
|
||||
"battery": SensorEntityDescription(
|
||||
key="battery",
|
||||
@@ -73,47 +68,40 @@ SENSORS: dict[str, SensorEntityDescription] = {
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
suggested_display_precision=0,
|
||||
),
|
||||
"co2": SensorEntityDescription(
|
||||
key="co2",
|
||||
device_class=SensorDeviceClass.CO2,
|
||||
native_unit_of_measurement=CONCENTRATION_PARTS_PER_MILLION,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
suggested_display_precision=0,
|
||||
),
|
||||
"voc": SensorEntityDescription(
|
||||
key="voc",
|
||||
device_class=SensorDeviceClass.VOLATILE_ORGANIC_COMPOUNDS_PARTS,
|
||||
native_unit_of_measurement=CONCENTRATION_PARTS_PER_BILLION,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
suggested_display_precision=0,
|
||||
),
|
||||
"light": SensorEntityDescription(
|
||||
key="light",
|
||||
native_unit_of_measurement=PERCENTAGE,
|
||||
translation_key="light",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
suggested_display_precision=0,
|
||||
),
|
||||
"lux": SensorEntityDescription(
|
||||
key="lux",
|
||||
device_class=SensorDeviceClass.ILLUMINANCE,
|
||||
native_unit_of_measurement=LIGHT_LUX,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
suggested_display_precision=0,
|
||||
),
|
||||
"virusRisk": SensorEntityDescription(
|
||||
key="virusRisk",
|
||||
translation_key="virus_risk",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
suggested_display_precision=0,
|
||||
),
|
||||
"mold": SensorEntityDescription(
|
||||
key="mold",
|
||||
translation_key="mold",
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
suggested_display_precision=0,
|
||||
),
|
||||
"rssi": SensorEntityDescription(
|
||||
key="rssi",
|
||||
@@ -122,21 +110,18 @@ SENSORS: dict[str, SensorEntityDescription] = {
|
||||
entity_registry_enabled_default=False,
|
||||
entity_category=EntityCategory.DIAGNOSTIC,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
suggested_display_precision=0,
|
||||
),
|
||||
"pm1": SensorEntityDescription(
|
||||
key="pm1",
|
||||
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
device_class=SensorDeviceClass.PM1,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
suggested_display_precision=0,
|
||||
),
|
||||
"pm25": SensorEntityDescription(
|
||||
key="pm25",
|
||||
native_unit_of_measurement=CONCENTRATION_MICROGRAMS_PER_CUBIC_METER,
|
||||
device_class=SensorDeviceClass.PM25,
|
||||
state_class=SensorStateClass.MEASUREMENT,
|
||||
suggested_display_precision=0,
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
"""Alexa Devices integration."""
|
||||
"""Amazon Devices integration."""
|
||||
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
@@ -13,7 +13,7 @@ PLATFORMS = [
|
||||
|
||||
|
||||
async def async_setup_entry(hass: HomeAssistant, entry: AmazonConfigEntry) -> bool:
|
||||
"""Set up Alexa Devices platform."""
|
||||
"""Set up Amazon Devices platform."""
|
||||
|
||||
coordinator = AmazonDevicesCoordinator(hass, entry)
|
||||
|
||||
+2
-2
@@ -26,7 +26,7 @@ PARALLEL_UPDATES = 0
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class AmazonBinarySensorEntityDescription(BinarySensorEntityDescription):
|
||||
"""Alexa Devices binary sensor entity description."""
|
||||
"""Amazon Devices binary sensor entity description."""
|
||||
|
||||
is_on_fn: Callable[[AmazonDevice], bool]
|
||||
|
||||
@@ -52,7 +52,7 @@ async def async_setup_entry(
|
||||
entry: AmazonConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up Alexa Devices binary sensors based on a config entry."""
|
||||
"""Set up Amazon Devices binary sensors based on a config entry."""
|
||||
|
||||
coordinator = entry.runtime_data
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
"""Config flow for Alexa Devices integration."""
|
||||
"""Config flow for Amazon Devices integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -17,7 +17,7 @@ from .const import CONF_LOGIN_DATA, DOMAIN
|
||||
|
||||
|
||||
class AmazonDevicesConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
"""Handle a config flow for Alexa Devices."""
|
||||
"""Handle a config flow for Amazon Devices."""
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
+2
-2
@@ -1,8 +1,8 @@
|
||||
"""Alexa Devices constants."""
|
||||
"""Amazon Devices constants."""
|
||||
|
||||
import logging
|
||||
|
||||
_LOGGER = logging.getLogger(__package__)
|
||||
|
||||
DOMAIN = "alexa_devices"
|
||||
DOMAIN = "amazon_devices"
|
||||
CONF_LOGIN_DATA = "login_data"
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
"""Support for Alexa Devices."""
|
||||
"""Support for Amazon Devices."""
|
||||
|
||||
from datetime import timedelta
|
||||
|
||||
@@ -23,7 +23,7 @@ type AmazonConfigEntry = ConfigEntry[AmazonDevicesCoordinator]
|
||||
|
||||
|
||||
class AmazonDevicesCoordinator(DataUpdateCoordinator[dict[str, AmazonDevice]]):
|
||||
"""Base coordinator for Alexa Devices."""
|
||||
"""Base coordinator for Amazon Devices."""
|
||||
|
||||
config_entry: AmazonConfigEntry
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
"""Diagnostics support for Alexa Devices integration."""
|
||||
"""Diagnostics support for Amazon Devices integration."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
"""Defines a base Alexa Devices entity."""
|
||||
"""Defines a base Amazon Devices entity."""
|
||||
|
||||
from aioamazondevices.api import AmazonDevice
|
||||
from aioamazondevices.const import SPEAKER_GROUP_MODEL
|
||||
@@ -12,7 +12,7 @@ from .coordinator import AmazonDevicesCoordinator
|
||||
|
||||
|
||||
class AmazonEntity(CoordinatorEntity[AmazonDevicesCoordinator]):
|
||||
"""Defines a base Alexa Devices entity."""
|
||||
"""Defines a base Amazon Devices entity."""
|
||||
|
||||
_attr_has_entity_name = True
|
||||
|
||||
+3
-3
@@ -1,9 +1,9 @@
|
||||
{
|
||||
"domain": "alexa_devices",
|
||||
"name": "Alexa Devices",
|
||||
"domain": "amazon_devices",
|
||||
"name": "Amazon Devices",
|
||||
"codeowners": ["@chemelli74"],
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/alexa_devices",
|
||||
"documentation": "https://www.home-assistant.io/integrations/amazon_devices",
|
||||
"integration_type": "hub",
|
||||
"iot_class": "cloud_polling",
|
||||
"loggers": ["aioamazondevices"],
|
||||
+2
-2
@@ -20,7 +20,7 @@ PARALLEL_UPDATES = 1
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class AmazonNotifyEntityDescription(NotifyEntityDescription):
|
||||
"""Alexa Devices notify entity description."""
|
||||
"""Amazon Devices notify entity description."""
|
||||
|
||||
method: Callable[[AmazonEchoApi, AmazonDevice, str], Awaitable[None]]
|
||||
subkey: str
|
||||
@@ -49,7 +49,7 @@ async def async_setup_entry(
|
||||
entry: AmazonConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up Alexa Devices notification entity based on a config entry."""
|
||||
"""Set up Amazon Devices notification entity based on a config entry."""
|
||||
|
||||
coordinator = entry.runtime_data
|
||||
|
||||
+6
-6
@@ -12,16 +12,16 @@
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"country": "[%key:component::alexa_devices::common::data_country%]",
|
||||
"country": "[%key:component::amazon_devices::common::data_country%]",
|
||||
"username": "[%key:common::config_flow::data::username%]",
|
||||
"password": "[%key:common::config_flow::data::password%]",
|
||||
"code": "[%key:component::alexa_devices::common::data_description_code%]"
|
||||
"code": "[%key:component::amazon_devices::common::data_description_code%]"
|
||||
},
|
||||
"data_description": {
|
||||
"country": "[%key:component::alexa_devices::common::data_description_country%]",
|
||||
"username": "[%key:component::alexa_devices::common::data_description_username%]",
|
||||
"password": "[%key:component::alexa_devices::common::data_description_password%]",
|
||||
"code": "[%key:component::alexa_devices::common::data_description_code%]"
|
||||
"country": "[%key:component::amazon_devices::common::data_description_country%]",
|
||||
"username": "[%key:component::amazon_devices::common::data_description_username%]",
|
||||
"password": "[%key:component::amazon_devices::common::data_description_password%]",
|
||||
"code": "[%key:component::amazon_devices::common::data_description_code%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
+2
-2
@@ -20,7 +20,7 @@ PARALLEL_UPDATES = 1
|
||||
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class AmazonSwitchEntityDescription(SwitchEntityDescription):
|
||||
"""Alexa Devices switch entity description."""
|
||||
"""Amazon Devices switch entity description."""
|
||||
|
||||
is_on_fn: Callable[[AmazonDevice], bool]
|
||||
subkey: str
|
||||
@@ -43,7 +43,7 @@ async def async_setup_entry(
|
||||
entry: AmazonConfigEntry,
|
||||
async_add_entities: AddConfigEntryEntitiesCallback,
|
||||
) -> None:
|
||||
"""Set up Alexa Devices switches based on a config entry."""
|
||||
"""Set up Amazon Devices switches based on a config entry."""
|
||||
|
||||
coordinator = entry.runtime_data
|
||||
|
||||
@@ -25,7 +25,7 @@ from homeassistant.helpers.typing import ConfigType
|
||||
|
||||
from .const import DEFAULT_SCAN_INTERVAL, DOMAIN, PLATFORMS
|
||||
from .coordinator import BlinkConfigEntry, BlinkUpdateCoordinator
|
||||
from .services import async_setup_services
|
||||
from .services import setup_services
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -72,7 +72,7 @@ async def async_migrate_entry(hass: HomeAssistant, entry: BlinkConfigEntry) -> b
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up Blink."""
|
||||
|
||||
async_setup_services(hass)
|
||||
setup_services(hass)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigEntryState
|
||||
from homeassistant.const import CONF_PIN
|
||||
from homeassistant.core import HomeAssistant, ServiceCall, callback
|
||||
from homeassistant.core import HomeAssistant, ServiceCall
|
||||
from homeassistant.exceptions import HomeAssistantError, ServiceValidationError
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
|
||||
@@ -21,36 +21,34 @@ SERVICE_SEND_PIN_SCHEMA = vol.Schema(
|
||||
)
|
||||
|
||||
|
||||
async def _send_pin(call: ServiceCall) -> None:
|
||||
"""Call blink to send new pin."""
|
||||
config_entry: BlinkConfigEntry | None
|
||||
for entry_id in call.data[ATTR_CONFIG_ENTRY_ID]:
|
||||
if not (config_entry := call.hass.config_entries.async_get_entry(entry_id)):
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="integration_not_found",
|
||||
translation_placeholders={"target": DOMAIN},
|
||||
)
|
||||
if config_entry.state != ConfigEntryState.LOADED:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="not_loaded",
|
||||
translation_placeholders={"target": config_entry.title},
|
||||
)
|
||||
coordinator = config_entry.runtime_data
|
||||
await coordinator.api.auth.send_auth_key(
|
||||
coordinator.api,
|
||||
call.data[CONF_PIN],
|
||||
)
|
||||
|
||||
|
||||
@callback
|
||||
def async_setup_services(hass: HomeAssistant) -> None:
|
||||
def setup_services(hass: HomeAssistant) -> None:
|
||||
"""Set up the services for the Blink integration."""
|
||||
|
||||
async def send_pin(call: ServiceCall):
|
||||
"""Call blink to send new pin."""
|
||||
config_entry: BlinkConfigEntry | None
|
||||
for entry_id in call.data[ATTR_CONFIG_ENTRY_ID]:
|
||||
if not (config_entry := hass.config_entries.async_get_entry(entry_id)):
|
||||
raise ServiceValidationError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="integration_not_found",
|
||||
translation_placeholders={"target": DOMAIN},
|
||||
)
|
||||
if config_entry.state != ConfigEntryState.LOADED:
|
||||
raise HomeAssistantError(
|
||||
translation_domain=DOMAIN,
|
||||
translation_key="not_loaded",
|
||||
translation_placeholders={"target": config_entry.title},
|
||||
)
|
||||
coordinator = config_entry.runtime_data
|
||||
await coordinator.api.auth.send_auth_key(
|
||||
coordinator.api,
|
||||
call.data[CONF_PIN],
|
||||
)
|
||||
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
SERVICE_SEND_PIN,
|
||||
_send_pin,
|
||||
send_pin,
|
||||
schema=SERVICE_SEND_PIN_SCHEMA,
|
||||
)
|
||||
|
||||
@@ -9,11 +9,12 @@ from typing import Any
|
||||
from homeassistant.components.notify import BaseNotificationService
|
||||
from homeassistant.const import CONF_COMMAND
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import TemplateError
|
||||
from homeassistant.helpers.template import Template
|
||||
from homeassistant.helpers.typing import ConfigType, DiscoveryInfoType
|
||||
from homeassistant.util.process import kill_subprocess
|
||||
|
||||
from .const import CONF_COMMAND_TIMEOUT, LOGGER
|
||||
from .utils import render_template_args
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -44,10 +45,28 @@ class CommandLineNotificationService(BaseNotificationService):
|
||||
|
||||
def send_message(self, message: str = "", **kwargs: Any) -> None:
|
||||
"""Send a message to a command line."""
|
||||
if not (command := render_template_args(self.hass, self.command)):
|
||||
return
|
||||
command = self.command
|
||||
if " " not in command:
|
||||
prog = command
|
||||
args = None
|
||||
args_compiled = None
|
||||
else:
|
||||
prog, args = command.split(" ", 1)
|
||||
args_compiled = Template(args, self.hass)
|
||||
|
||||
LOGGER.debug("Running with message: %s", message)
|
||||
rendered_args = None
|
||||
if args_compiled:
|
||||
args_to_render = {"arguments": args}
|
||||
try:
|
||||
rendered_args = args_compiled.async_render(args_to_render)
|
||||
except TemplateError as ex:
|
||||
LOGGER.exception("Error rendering command template: %s", ex)
|
||||
return
|
||||
|
||||
if rendered_args != args:
|
||||
command = f"{prog} {rendered_args}"
|
||||
|
||||
LOGGER.debug("Running command: %s, with message: %s", command, message)
|
||||
|
||||
with subprocess.Popen( # noqa: S602 # shell by design
|
||||
command,
|
||||
|
||||
@@ -19,6 +19,7 @@ from homeassistant.const import (
|
||||
CONF_VALUE_TEMPLATE,
|
||||
)
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import TemplateError
|
||||
from homeassistant.helpers.entity_platform import AddEntitiesCallback
|
||||
from homeassistant.helpers.event import async_track_time_interval
|
||||
from homeassistant.helpers.template import Template
|
||||
@@ -36,7 +37,7 @@ from .const import (
|
||||
LOGGER,
|
||||
TRIGGER_ENTITY_OPTIONS,
|
||||
)
|
||||
from .utils import async_check_output_or_log, render_template_args
|
||||
from .utils import async_check_output_or_log
|
||||
|
||||
DEFAULT_NAME = "Command Sensor"
|
||||
|
||||
@@ -221,6 +222,32 @@ class CommandSensorData:
|
||||
|
||||
async def async_update(self) -> None:
|
||||
"""Get the latest data with a shell command."""
|
||||
if not (command := render_template_args(self.hass, self.command)):
|
||||
return
|
||||
command = self.command
|
||||
|
||||
if " " not in command:
|
||||
prog = command
|
||||
args = None
|
||||
args_compiled = None
|
||||
else:
|
||||
prog, args = command.split(" ", 1)
|
||||
args_compiled = Template(args, self.hass)
|
||||
|
||||
if args_compiled:
|
||||
try:
|
||||
args_to_render = {"arguments": args}
|
||||
rendered_args = args_compiled.async_render(args_to_render)
|
||||
except TemplateError as ex:
|
||||
LOGGER.exception("Error rendering command template: %s", ex)
|
||||
return
|
||||
else:
|
||||
rendered_args = None
|
||||
|
||||
if rendered_args == args:
|
||||
# No template used. default behavior
|
||||
pass
|
||||
else:
|
||||
# Template used. Construct the string used in the shell
|
||||
command = f"{prog} {rendered_args}"
|
||||
|
||||
LOGGER.debug("Running command: %s", command)
|
||||
self.value = await async_check_output_or_log(command, self.timeout)
|
||||
|
||||
@@ -3,13 +3,9 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.exceptions import TemplateError
|
||||
from homeassistant.helpers.template import Template
|
||||
|
||||
from .const import LOGGER
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
_EXEC_FAILED_CODE = 127
|
||||
|
||||
|
||||
@@ -22,7 +18,7 @@ async def async_call_shell_with_timeout(
|
||||
return code is returned.
|
||||
"""
|
||||
try:
|
||||
LOGGER.debug("Running command: %s", command)
|
||||
_LOGGER.debug("Running command: %s", command)
|
||||
proc = await asyncio.create_subprocess_shell( # shell by design
|
||||
command,
|
||||
close_fds=False, # required for posix_spawn
|
||||
@@ -30,14 +26,14 @@ async def async_call_shell_with_timeout(
|
||||
async with asyncio.timeout(timeout):
|
||||
await proc.communicate()
|
||||
except TimeoutError:
|
||||
LOGGER.error("Timeout for command: %s", command)
|
||||
_LOGGER.error("Timeout for command: %s", command)
|
||||
return -1
|
||||
|
||||
return_code = proc.returncode
|
||||
if return_code == _EXEC_FAILED_CODE:
|
||||
LOGGER.error("Error trying to exec command: %s", command)
|
||||
_LOGGER.error("Error trying to exec command: %s", command)
|
||||
elif log_return_code and return_code != 0:
|
||||
LOGGER.error(
|
||||
_LOGGER.error(
|
||||
"Command failed (with return code %s): %s",
|
||||
proc.returncode,
|
||||
command,
|
||||
@@ -57,39 +53,12 @@ async def async_check_output_or_log(command: str, timeout: int) -> str | None:
|
||||
stdout, _ = await proc.communicate()
|
||||
|
||||
if proc.returncode != 0:
|
||||
LOGGER.error(
|
||||
_LOGGER.error(
|
||||
"Command failed (with return code %s): %s", proc.returncode, command
|
||||
)
|
||||
else:
|
||||
return stdout.strip().decode("utf-8")
|
||||
except TimeoutError:
|
||||
LOGGER.error("Timeout for command: %s", command)
|
||||
_LOGGER.error("Timeout for command: %s", command)
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def render_template_args(hass: HomeAssistant, command: str) -> str | None:
|
||||
"""Render template arguments for command line utilities."""
|
||||
if " " not in command:
|
||||
prog = command
|
||||
args = None
|
||||
args_compiled = None
|
||||
else:
|
||||
prog, args = command.split(" ", 1)
|
||||
args_compiled = Template(args, hass)
|
||||
|
||||
rendered_args = None
|
||||
if args_compiled:
|
||||
args_to_render = {"arguments": args}
|
||||
try:
|
||||
rendered_args = args_compiled.async_render(args_to_render)
|
||||
except TemplateError as ex:
|
||||
LOGGER.exception("Error rendering command template: %s", ex)
|
||||
return None
|
||||
|
||||
if rendered_args != args:
|
||||
command = f"{prog} {rendered_args}"
|
||||
|
||||
LOGGER.debug("Running command: %s", command)
|
||||
|
||||
return command
|
||||
|
||||
@@ -28,36 +28,45 @@ async def async_setup_entry(
|
||||
) -> None:
|
||||
"""Set up the Fibaro covers."""
|
||||
controller = entry.runtime_data
|
||||
|
||||
entities: list[FibaroEntity] = []
|
||||
for device in controller.fibaro_devices[Platform.COVER]:
|
||||
# Positionable covers report the position over value
|
||||
if device.value.has_value:
|
||||
entities.append(PositionableFibaroCover(device))
|
||||
else:
|
||||
entities.append(FibaroCover(device))
|
||||
async_add_entities(entities, True)
|
||||
async_add_entities(
|
||||
[FibaroCover(device) for device in controller.fibaro_devices[Platform.COVER]],
|
||||
True,
|
||||
)
|
||||
|
||||
|
||||
class PositionableFibaroCover(FibaroEntity, CoverEntity):
|
||||
"""Representation of a fibaro cover which supports positioning."""
|
||||
class FibaroCover(FibaroEntity, CoverEntity):
|
||||
"""Representation a Fibaro Cover."""
|
||||
|
||||
def __init__(self, fibaro_device: DeviceModel) -> None:
|
||||
"""Initialize the device."""
|
||||
"""Initialize the Vera device."""
|
||||
super().__init__(fibaro_device)
|
||||
self.entity_id = ENTITY_ID_FORMAT.format(self.ha_id)
|
||||
|
||||
if self._is_open_close_only():
|
||||
self._attr_supported_features = (
|
||||
CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE
|
||||
)
|
||||
if "stop" in self.fibaro_device.actions:
|
||||
self._attr_supported_features |= CoverEntityFeature.STOP
|
||||
|
||||
@staticmethod
|
||||
def bound(position: int | None) -> int | None:
|
||||
def bound(position):
|
||||
"""Normalize the position."""
|
||||
if position is None:
|
||||
return None
|
||||
position = int(position)
|
||||
if position <= 5:
|
||||
return 0
|
||||
if position >= 95:
|
||||
return 100
|
||||
return position
|
||||
|
||||
def _is_open_close_only(self) -> bool:
|
||||
"""Return if only open / close is supported."""
|
||||
# Normally positionable devices report the position over value,
|
||||
# so if it is missing we have a device which supports open / close only
|
||||
return not self.fibaro_device.value.has_value
|
||||
|
||||
def update(self) -> None:
|
||||
"""Update the state."""
|
||||
super().update()
|
||||
@@ -65,15 +74,20 @@ class PositionableFibaroCover(FibaroEntity, CoverEntity):
|
||||
self._attr_current_cover_position = self.bound(self.level)
|
||||
self._attr_current_cover_tilt_position = self.bound(self.level2)
|
||||
|
||||
device_state = self.fibaro_device.state
|
||||
|
||||
# Be aware that opening and closing is only available for some modern
|
||||
# devices.
|
||||
# For example the Fibaro Roller Shutter 4 reports this correctly.
|
||||
device_state = self.fibaro_device.state.str_value(default="").lower()
|
||||
self._attr_is_opening = device_state == "opening"
|
||||
self._attr_is_closing = device_state == "closing"
|
||||
if device_state.has_value:
|
||||
self._attr_is_opening = device_state.str_value().lower() == "opening"
|
||||
self._attr_is_closing = device_state.str_value().lower() == "closing"
|
||||
|
||||
closed: bool | None = None
|
||||
if self.current_cover_position is not None:
|
||||
if self._is_open_close_only():
|
||||
if device_state.has_value and device_state.str_value().lower() != "unknown":
|
||||
closed = device_state.str_value().lower() == "closed"
|
||||
elif self.current_cover_position is not None:
|
||||
closed = self.current_cover_position == 0
|
||||
self._attr_is_closed = closed
|
||||
|
||||
@@ -82,7 +96,7 @@ class PositionableFibaroCover(FibaroEntity, CoverEntity):
|
||||
self.set_level(cast(int, kwargs.get(ATTR_POSITION)))
|
||||
|
||||
def set_cover_tilt_position(self, **kwargs: Any) -> None:
|
||||
"""Move the slats to a specific position."""
|
||||
"""Move the cover to a specific position."""
|
||||
self.set_level2(cast(int, kwargs.get(ATTR_TILT_POSITION)))
|
||||
|
||||
def open_cover(self, **kwargs: Any) -> None:
|
||||
@@ -104,62 +118,3 @@ class PositionableFibaroCover(FibaroEntity, CoverEntity):
|
||||
def stop_cover(self, **kwargs: Any) -> None:
|
||||
"""Stop the cover."""
|
||||
self.action("stop")
|
||||
|
||||
|
||||
class FibaroCover(FibaroEntity, CoverEntity):
|
||||
"""Representation of a fibaro cover which supports only open / close commands."""
|
||||
|
||||
def __init__(self, fibaro_device: DeviceModel) -> None:
|
||||
"""Initialize the device."""
|
||||
super().__init__(fibaro_device)
|
||||
self.entity_id = ENTITY_ID_FORMAT.format(self.ha_id)
|
||||
|
||||
self._attr_supported_features = (
|
||||
CoverEntityFeature.OPEN | CoverEntityFeature.CLOSE
|
||||
)
|
||||
if "stop" in self.fibaro_device.actions:
|
||||
self._attr_supported_features |= CoverEntityFeature.STOP
|
||||
if "rotateSlatsUp" in self.fibaro_device.actions:
|
||||
self._attr_supported_features |= CoverEntityFeature.OPEN_TILT
|
||||
if "rotateSlatsDown" in self.fibaro_device.actions:
|
||||
self._attr_supported_features |= CoverEntityFeature.CLOSE_TILT
|
||||
if "stopSlats" in self.fibaro_device.actions:
|
||||
self._attr_supported_features |= CoverEntityFeature.STOP_TILT
|
||||
|
||||
def update(self) -> None:
|
||||
"""Update the state."""
|
||||
super().update()
|
||||
|
||||
device_state = self.fibaro_device.state.str_value(default="").lower()
|
||||
|
||||
self._attr_is_opening = device_state == "opening"
|
||||
self._attr_is_closing = device_state == "closing"
|
||||
|
||||
closed: bool | None = None
|
||||
if device_state not in {"", "unknown"}:
|
||||
closed = device_state == "closed"
|
||||
self._attr_is_closed = closed
|
||||
|
||||
def open_cover(self, **kwargs: Any) -> None:
|
||||
"""Open the cover."""
|
||||
self.action("open")
|
||||
|
||||
def close_cover(self, **kwargs: Any) -> None:
|
||||
"""Close the cover."""
|
||||
self.action("close")
|
||||
|
||||
def stop_cover(self, **kwargs: Any) -> None:
|
||||
"""Stop the cover."""
|
||||
self.action("stop")
|
||||
|
||||
def open_cover_tilt(self, **kwargs: Any) -> None:
|
||||
"""Open the cover slats."""
|
||||
self.action("rotateSlatsUp")
|
||||
|
||||
def close_cover_tilt(self, **kwargs: Any) -> None:
|
||||
"""Close the cover slats."""
|
||||
self.action("rotateSlatsDown")
|
||||
|
||||
def stop_cover_tilt(self, **kwargs: Any) -> None:
|
||||
"""Stop the cover slats turning."""
|
||||
self.action("stopSlats")
|
||||
|
||||
@@ -20,5 +20,5 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/frontend",
|
||||
"integration_type": "system",
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["home-assistant-frontend==20250531.2"]
|
||||
"requirements": ["home-assistant-frontend==20250531.0"]
|
||||
}
|
||||
|
||||
@@ -27,7 +27,7 @@ CONFIG_SCHEMA = cv.config_entry_only_config_schema(DOMAIN)
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up Fully Kiosk Browser."""
|
||||
|
||||
async_setup_services(hass)
|
||||
await async_setup_services(hass)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry, ConfigEntryState
|
||||
from homeassistant.const import ATTR_DEVICE_ID
|
||||
from homeassistant.core import HomeAssistant, ServiceCall, callback
|
||||
from homeassistant.core import HomeAssistant, ServiceCall
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.helpers import config_validation as cv, device_registry as dr
|
||||
|
||||
@@ -23,73 +23,71 @@ from .const import (
|
||||
from .coordinator import FullyKioskDataUpdateCoordinator
|
||||
|
||||
|
||||
async def _collect_coordinators(
|
||||
call: ServiceCall,
|
||||
) -> list[FullyKioskDataUpdateCoordinator]:
|
||||
device_ids: list[str] = call.data[ATTR_DEVICE_ID]
|
||||
config_entries = list[ConfigEntry]()
|
||||
registry = dr.async_get(call.hass)
|
||||
for target in device_ids:
|
||||
device = registry.async_get(target)
|
||||
if device:
|
||||
device_entries = list[ConfigEntry]()
|
||||
for entry_id in device.config_entries:
|
||||
entry = call.hass.config_entries.async_get_entry(entry_id)
|
||||
if entry and entry.domain == DOMAIN:
|
||||
device_entries.append(entry)
|
||||
if not device_entries:
|
||||
raise HomeAssistantError(f"Device '{target}' is not a {DOMAIN} device")
|
||||
config_entries.extend(device_entries)
|
||||
else:
|
||||
raise HomeAssistantError(f"Device '{target}' not found in device registry")
|
||||
coordinators = list[FullyKioskDataUpdateCoordinator]()
|
||||
for config_entry in config_entries:
|
||||
if config_entry.state != ConfigEntryState.LOADED:
|
||||
raise HomeAssistantError(f"{config_entry.title} is not loaded")
|
||||
coordinators.append(config_entry.runtime_data)
|
||||
return coordinators
|
||||
|
||||
|
||||
async def _async_load_url(call: ServiceCall) -> None:
|
||||
"""Load a URL on the Fully Kiosk Browser."""
|
||||
for coordinator in await _collect_coordinators(call):
|
||||
await coordinator.fully.loadUrl(call.data[ATTR_URL])
|
||||
|
||||
|
||||
async def _async_start_app(call: ServiceCall) -> None:
|
||||
"""Start an app on the device."""
|
||||
for coordinator in await _collect_coordinators(call):
|
||||
await coordinator.fully.startApplication(call.data[ATTR_APPLICATION])
|
||||
|
||||
|
||||
async def _async_set_config(call: ServiceCall) -> None:
|
||||
"""Set a Fully Kiosk Browser config value on the device."""
|
||||
for coordinator in await _collect_coordinators(call):
|
||||
key = call.data[ATTR_KEY]
|
||||
value = call.data[ATTR_VALUE]
|
||||
|
||||
# Fully API has different methods for setting string and bool values.
|
||||
# check if call.data[ATTR_VALUE] is a bool
|
||||
if isinstance(value, bool) or (
|
||||
isinstance(value, str) and value.lower() in ("true", "false")
|
||||
):
|
||||
await coordinator.fully.setConfigurationBool(key, value)
|
||||
else:
|
||||
# Convert any int values to string
|
||||
if isinstance(value, int):
|
||||
value = str(value)
|
||||
|
||||
await coordinator.fully.setConfigurationString(key, value)
|
||||
|
||||
|
||||
@callback
|
||||
def async_setup_services(hass: HomeAssistant) -> None:
|
||||
async def async_setup_services(hass: HomeAssistant) -> None:
|
||||
"""Set up the services for the Fully Kiosk Browser integration."""
|
||||
|
||||
async def collect_coordinators(
|
||||
device_ids: list[str],
|
||||
) -> list[FullyKioskDataUpdateCoordinator]:
|
||||
config_entries = list[ConfigEntry]()
|
||||
registry = dr.async_get(hass)
|
||||
for target in device_ids:
|
||||
device = registry.async_get(target)
|
||||
if device:
|
||||
device_entries = list[ConfigEntry]()
|
||||
for entry_id in device.config_entries:
|
||||
entry = hass.config_entries.async_get_entry(entry_id)
|
||||
if entry and entry.domain == DOMAIN:
|
||||
device_entries.append(entry)
|
||||
if not device_entries:
|
||||
raise HomeAssistantError(
|
||||
f"Device '{target}' is not a {DOMAIN} device"
|
||||
)
|
||||
config_entries.extend(device_entries)
|
||||
else:
|
||||
raise HomeAssistantError(
|
||||
f"Device '{target}' not found in device registry"
|
||||
)
|
||||
coordinators = list[FullyKioskDataUpdateCoordinator]()
|
||||
for config_entry in config_entries:
|
||||
if config_entry.state != ConfigEntryState.LOADED:
|
||||
raise HomeAssistantError(f"{config_entry.title} is not loaded")
|
||||
coordinators.append(config_entry.runtime_data)
|
||||
return coordinators
|
||||
|
||||
async def async_load_url(call: ServiceCall) -> None:
|
||||
"""Load a URL on the Fully Kiosk Browser."""
|
||||
for coordinator in await collect_coordinators(call.data[ATTR_DEVICE_ID]):
|
||||
await coordinator.fully.loadUrl(call.data[ATTR_URL])
|
||||
|
||||
async def async_start_app(call: ServiceCall) -> None:
|
||||
"""Start an app on the device."""
|
||||
for coordinator in await collect_coordinators(call.data[ATTR_DEVICE_ID]):
|
||||
await coordinator.fully.startApplication(call.data[ATTR_APPLICATION])
|
||||
|
||||
async def async_set_config(call: ServiceCall) -> None:
|
||||
"""Set a Fully Kiosk Browser config value on the device."""
|
||||
for coordinator in await collect_coordinators(call.data[ATTR_DEVICE_ID]):
|
||||
key = call.data[ATTR_KEY]
|
||||
value = call.data[ATTR_VALUE]
|
||||
|
||||
# Fully API has different methods for setting string and bool values.
|
||||
# check if call.data[ATTR_VALUE] is a bool
|
||||
if isinstance(value, bool) or (
|
||||
isinstance(value, str) and value.lower() in ("true", "false")
|
||||
):
|
||||
await coordinator.fully.setConfigurationBool(key, value)
|
||||
else:
|
||||
# Convert any int values to string
|
||||
if isinstance(value, int):
|
||||
value = str(value)
|
||||
|
||||
await coordinator.fully.setConfigurationString(key, value)
|
||||
|
||||
# Register all the above services
|
||||
service_mapping = [
|
||||
(_async_load_url, SERVICE_LOAD_URL, ATTR_URL),
|
||||
(_async_start_app, SERVICE_START_APPLICATION, ATTR_APPLICATION),
|
||||
(async_load_url, SERVICE_LOAD_URL, ATTR_URL),
|
||||
(async_start_app, SERVICE_START_APPLICATION, ATTR_APPLICATION),
|
||||
]
|
||||
for service_handler, service_name, attrib in service_mapping:
|
||||
hass.services.async_register(
|
||||
@@ -109,7 +107,7 @@ def async_setup_services(hass: HomeAssistant) -> None:
|
||||
hass.services.async_register(
|
||||
DOMAIN,
|
||||
SERVICE_SET_CONFIG,
|
||||
_async_set_config,
|
||||
async_set_config,
|
||||
schema=vol.Schema(
|
||||
vol.All(
|
||||
{
|
||||
|
||||
@@ -27,7 +27,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
"""Set up the Google Mail integration."""
|
||||
hass.data.setdefault(DOMAIN, {})[DATA_HASS_CONFIG] = config
|
||||
|
||||
async_setup_services(hass)
|
||||
await async_setup_services(hass)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ from typing import TYPE_CHECKING
|
||||
from googleapiclient.http import HttpRequest
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.core import HomeAssistant, ServiceCall, callback
|
||||
from homeassistant.core import HomeAssistant, ServiceCall
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.service import async_extract_config_entry_ids
|
||||
|
||||
@@ -46,57 +46,56 @@ SERVICE_VACATION_SCHEMA = vol.All(
|
||||
)
|
||||
|
||||
|
||||
async def _extract_gmail_config_entries(
|
||||
call: ServiceCall,
|
||||
) -> list[GoogleMailConfigEntry]:
|
||||
return [
|
||||
entry
|
||||
for entry_id in await async_extract_config_entry_ids(call.hass, call)
|
||||
if (entry := call.hass.config_entries.async_get_entry(entry_id))
|
||||
and entry.domain == DOMAIN
|
||||
]
|
||||
|
||||
|
||||
async def _gmail_service(call: ServiceCall) -> None:
|
||||
"""Call Google Mail service."""
|
||||
for entry in await _extract_gmail_config_entries(call):
|
||||
try:
|
||||
auth = entry.runtime_data
|
||||
except AttributeError as ex:
|
||||
raise ValueError(f"Config entry not loaded: {entry.entry_id}") from ex
|
||||
service = await auth.get_resource()
|
||||
|
||||
_settings = {
|
||||
"enableAutoReply": call.data[ATTR_ENABLED],
|
||||
"responseSubject": call.data.get(ATTR_TITLE),
|
||||
}
|
||||
if contacts := call.data.get(ATTR_RESTRICT_CONTACTS):
|
||||
_settings["restrictToContacts"] = contacts
|
||||
if domain := call.data.get(ATTR_RESTRICT_DOMAIN):
|
||||
_settings["restrictToDomain"] = domain
|
||||
if _date := call.data.get(ATTR_START):
|
||||
_dt = datetime.combine(_date, datetime.min.time())
|
||||
_settings["startTime"] = _dt.timestamp() * 1000
|
||||
if _date := call.data.get(ATTR_END):
|
||||
_dt = datetime.combine(_date, datetime.min.time())
|
||||
_settings["endTime"] = (_dt + timedelta(days=1)).timestamp() * 1000
|
||||
if call.data[ATTR_PLAIN_TEXT]:
|
||||
_settings["responseBodyPlainText"] = call.data[ATTR_MESSAGE]
|
||||
else:
|
||||
_settings["responseBodyHtml"] = call.data[ATTR_MESSAGE]
|
||||
settings: HttpRequest = (
|
||||
service.users().settings().updateVacation(userId=ATTR_ME, body=_settings)
|
||||
)
|
||||
await call.hass.async_add_executor_job(settings.execute)
|
||||
|
||||
|
||||
@callback
|
||||
def async_setup_services(hass: HomeAssistant) -> None:
|
||||
async def async_setup_services(hass: HomeAssistant) -> None:
|
||||
"""Set up services for Google Mail integration."""
|
||||
|
||||
async def extract_gmail_config_entries(
|
||||
call: ServiceCall,
|
||||
) -> list[GoogleMailConfigEntry]:
|
||||
return [
|
||||
entry
|
||||
for entry_id in await async_extract_config_entry_ids(hass, call)
|
||||
if (entry := hass.config_entries.async_get_entry(entry_id))
|
||||
and entry.domain == DOMAIN
|
||||
]
|
||||
|
||||
async def gmail_service(call: ServiceCall) -> None:
|
||||
"""Call Google Mail service."""
|
||||
for entry in await extract_gmail_config_entries(call):
|
||||
try:
|
||||
auth = entry.runtime_data
|
||||
except AttributeError as ex:
|
||||
raise ValueError(f"Config entry not loaded: {entry.entry_id}") from ex
|
||||
service = await auth.get_resource()
|
||||
|
||||
_settings = {
|
||||
"enableAutoReply": call.data[ATTR_ENABLED],
|
||||
"responseSubject": call.data.get(ATTR_TITLE),
|
||||
}
|
||||
if contacts := call.data.get(ATTR_RESTRICT_CONTACTS):
|
||||
_settings["restrictToContacts"] = contacts
|
||||
if domain := call.data.get(ATTR_RESTRICT_DOMAIN):
|
||||
_settings["restrictToDomain"] = domain
|
||||
if _date := call.data.get(ATTR_START):
|
||||
_dt = datetime.combine(_date, datetime.min.time())
|
||||
_settings["startTime"] = _dt.timestamp() * 1000
|
||||
if _date := call.data.get(ATTR_END):
|
||||
_dt = datetime.combine(_date, datetime.min.time())
|
||||
_settings["endTime"] = (_dt + timedelta(days=1)).timestamp() * 1000
|
||||
if call.data[ATTR_PLAIN_TEXT]:
|
||||
_settings["responseBodyPlainText"] = call.data[ATTR_MESSAGE]
|
||||
else:
|
||||
_settings["responseBodyHtml"] = call.data[ATTR_MESSAGE]
|
||||
settings: HttpRequest = (
|
||||
service.users()
|
||||
.settings()
|
||||
.updateVacation(userId=ATTR_ME, body=_settings)
|
||||
)
|
||||
await hass.async_add_executor_job(settings.execute)
|
||||
|
||||
hass.services.async_register(
|
||||
domain=DOMAIN,
|
||||
service=SERVICE_SET_VACATION,
|
||||
schema=SERVICE_VACATION_SCHEMA,
|
||||
service_func=_gmail_service,
|
||||
service_func=gmail_service,
|
||||
)
|
||||
|
||||
@@ -9,10 +9,8 @@ from functools import partial
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
import struct
|
||||
from typing import Any, NamedTuple
|
||||
|
||||
import aiofiles
|
||||
from aiohasupervisor import SupervisorError
|
||||
import voluptuous as vol
|
||||
|
||||
@@ -58,6 +56,7 @@ from homeassistant.helpers.issue_registry import IssueSeverity
|
||||
from homeassistant.helpers.service_info.hassio import (
|
||||
HassioServiceInfo as _HassioServiceInfo,
|
||||
)
|
||||
from homeassistant.helpers.system_info import async_get_system_info
|
||||
from homeassistant.helpers.typing import ConfigType
|
||||
from homeassistant.loader import bind_hass
|
||||
from homeassistant.util.async_ import create_eager_task
|
||||
@@ -234,17 +233,6 @@ SCHEMA_RESTORE_PARTIAL = SCHEMA_RESTORE_FULL.extend(
|
||||
)
|
||||
|
||||
|
||||
def _is_32_bit() -> bool:
|
||||
size = struct.calcsize("P")
|
||||
return size * 8 == 32
|
||||
|
||||
|
||||
async def _get_arch() -> str:
|
||||
async with aiofiles.open("/etc/apk/arch") as arch_file:
|
||||
raw_arch = await arch_file.read()
|
||||
return {"x86": "i386"}.get(raw_arch, raw_arch)
|
||||
|
||||
|
||||
class APIEndpointSettings(NamedTuple):
|
||||
"""Settings for API endpoint."""
|
||||
|
||||
@@ -566,7 +554,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
await coordinator.async_config_entry_first_refresh()
|
||||
hass.data[ADDONS_COORDINATOR] = coordinator
|
||||
|
||||
arch = await _get_arch()
|
||||
system_info = await async_get_system_info(hass)
|
||||
|
||||
def deprecated_setup_issue() -> None:
|
||||
os_info = get_os_info(hass)
|
||||
@@ -574,19 +562,20 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
if os_info is None or info is None:
|
||||
return
|
||||
is_haos = info.get("hassos") is not None
|
||||
arch = system_info["arch"]
|
||||
board = os_info.get("board")
|
||||
unsupported_board = board in {"tinker", "odroid-xu4", "rpi2"}
|
||||
unsupported_os_on_board = board in {"rpi3", "rpi4"}
|
||||
if is_haos and (unsupported_board or unsupported_os_on_board):
|
||||
supported_board = board in {"rpi3", "rpi4", "tinker", "odroid-xu4", "rpi2"}
|
||||
if is_haos and arch == "armv7" and supported_board:
|
||||
issue_id = "deprecated_os_"
|
||||
if unsupported_os_on_board:
|
||||
if board in {"rpi3", "rpi4"}:
|
||||
issue_id += "aarch64"
|
||||
elif unsupported_board:
|
||||
elif board in {"tinker", "odroid-xu4", "rpi2"}:
|
||||
issue_id += "armv7"
|
||||
ir.async_create_issue(
|
||||
hass,
|
||||
"homeassistant",
|
||||
issue_id,
|
||||
breaks_in_ha_version="2025.12.0",
|
||||
learn_more_url=DEPRECATION_URL,
|
||||
is_fixable=False,
|
||||
severity=IssueSeverity.WARNING,
|
||||
@@ -595,10 +584,9 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
"installation_guide": "https://www.home-assistant.io/installation/",
|
||||
},
|
||||
)
|
||||
bit32 = _is_32_bit()
|
||||
deprecated_architecture = bit32 and not (
|
||||
unsupported_board or unsupported_os_on_board
|
||||
)
|
||||
deprecated_architecture = False
|
||||
if arch in {"i386", "armhf"} or (arch == "armv7" and not supported_board):
|
||||
deprecated_architecture = True
|
||||
if not is_haos or deprecated_architecture:
|
||||
issue_id = "deprecated"
|
||||
if not is_haos:
|
||||
@@ -609,6 +597,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
|
||||
hass,
|
||||
"homeassistant",
|
||||
issue_id,
|
||||
breaks_in_ha_version="2025.12.0",
|
||||
learn_more_url=DEPRECATION_URL,
|
||||
is_fixable=False,
|
||||
severity=IssueSeverity.WARNING,
|
||||
|
||||
@@ -5,13 +5,26 @@ from __future__ import annotations
|
||||
from homeassistant.const import CONF_API_KEY, CONF_MODE, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers.start import async_at_started
|
||||
from homeassistant.util import dt as dt_util
|
||||
|
||||
from .const import TRAVEL_MODE_PUBLIC
|
||||
from .const import (
|
||||
CONF_ARRIVAL_TIME,
|
||||
CONF_DEPARTURE_TIME,
|
||||
CONF_DESTINATION_ENTITY_ID,
|
||||
CONF_DESTINATION_LATITUDE,
|
||||
CONF_DESTINATION_LONGITUDE,
|
||||
CONF_ORIGIN_ENTITY_ID,
|
||||
CONF_ORIGIN_LATITUDE,
|
||||
CONF_ORIGIN_LONGITUDE,
|
||||
CONF_ROUTE_MODE,
|
||||
TRAVEL_MODE_PUBLIC,
|
||||
)
|
||||
from .coordinator import (
|
||||
HereConfigEntry,
|
||||
HERERoutingDataUpdateCoordinator,
|
||||
HERETransitDataUpdateCoordinator,
|
||||
)
|
||||
from .model import HERETravelTimeConfig
|
||||
|
||||
PLATFORMS = [Platform.SENSOR]
|
||||
|
||||
@@ -20,13 +33,29 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: HereConfigEntry)
|
||||
"""Set up HERE Travel Time from a config entry."""
|
||||
api_key = config_entry.data[CONF_API_KEY]
|
||||
|
||||
arrival = dt_util.parse_time(config_entry.options.get(CONF_ARRIVAL_TIME, ""))
|
||||
departure = dt_util.parse_time(config_entry.options.get(CONF_DEPARTURE_TIME, ""))
|
||||
|
||||
here_travel_time_config = HERETravelTimeConfig(
|
||||
destination_latitude=config_entry.data.get(CONF_DESTINATION_LATITUDE),
|
||||
destination_longitude=config_entry.data.get(CONF_DESTINATION_LONGITUDE),
|
||||
destination_entity_id=config_entry.data.get(CONF_DESTINATION_ENTITY_ID),
|
||||
origin_latitude=config_entry.data.get(CONF_ORIGIN_LATITUDE),
|
||||
origin_longitude=config_entry.data.get(CONF_ORIGIN_LONGITUDE),
|
||||
origin_entity_id=config_entry.data.get(CONF_ORIGIN_ENTITY_ID),
|
||||
travel_mode=config_entry.data[CONF_MODE],
|
||||
route_mode=config_entry.options[CONF_ROUTE_MODE],
|
||||
arrival=arrival,
|
||||
departure=departure,
|
||||
)
|
||||
|
||||
cls: type[HERETransitDataUpdateCoordinator | HERERoutingDataUpdateCoordinator]
|
||||
if config_entry.data[CONF_MODE] in {TRAVEL_MODE_PUBLIC, "publicTransportTimeTable"}:
|
||||
cls = HERETransitDataUpdateCoordinator
|
||||
else:
|
||||
cls = HERERoutingDataUpdateCoordinator
|
||||
|
||||
data_coordinator = cls(hass, config_entry, api_key)
|
||||
data_coordinator = cls(hass, config_entry, api_key, here_travel_time_config)
|
||||
config_entry.runtime_data = data_coordinator
|
||||
|
||||
async def _async_update_at_start(_: HomeAssistant) -> None:
|
||||
|
||||
@@ -26,7 +26,7 @@ from here_transit import (
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.const import CONF_MODE, UnitOfLength
|
||||
from homeassistant.const import UnitOfLength
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.location import find_coordinates
|
||||
@@ -34,21 +34,8 @@ from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, Upda
|
||||
from homeassistant.util import dt as dt_util
|
||||
from homeassistant.util.unit_conversion import DistanceConverter
|
||||
|
||||
from .const import (
|
||||
CONF_ARRIVAL_TIME,
|
||||
CONF_DEPARTURE_TIME,
|
||||
CONF_DESTINATION_ENTITY_ID,
|
||||
CONF_DESTINATION_LATITUDE,
|
||||
CONF_DESTINATION_LONGITUDE,
|
||||
CONF_ORIGIN_ENTITY_ID,
|
||||
CONF_ORIGIN_LATITUDE,
|
||||
CONF_ORIGIN_LONGITUDE,
|
||||
CONF_ROUTE_MODE,
|
||||
DEFAULT_SCAN_INTERVAL,
|
||||
DOMAIN,
|
||||
ROUTE_MODE_FASTEST,
|
||||
)
|
||||
from .model import HERETravelTimeAPIParams, HERETravelTimeData
|
||||
from .const import DEFAULT_SCAN_INTERVAL, DOMAIN, ROUTE_MODE_FASTEST
|
||||
from .model import HERETravelTimeConfig, HERETravelTimeData
|
||||
|
||||
BACKOFF_MULTIPLIER = 1.1
|
||||
|
||||
@@ -60,7 +47,7 @@ type HereConfigEntry = ConfigEntry[
|
||||
|
||||
|
||||
class HERERoutingDataUpdateCoordinator(DataUpdateCoordinator[HERETravelTimeData]):
|
||||
"""HERETravelTime DataUpdateCoordinator for the routing API."""
|
||||
"""here_routing DataUpdateCoordinator."""
|
||||
|
||||
config_entry: HereConfigEntry
|
||||
|
||||
@@ -69,6 +56,7 @@ class HERERoutingDataUpdateCoordinator(DataUpdateCoordinator[HERETravelTimeData]
|
||||
hass: HomeAssistant,
|
||||
config_entry: HereConfigEntry,
|
||||
api_key: str,
|
||||
config: HERETravelTimeConfig,
|
||||
) -> None:
|
||||
"""Initialize."""
|
||||
super().__init__(
|
||||
@@ -79,34 +67,41 @@ class HERERoutingDataUpdateCoordinator(DataUpdateCoordinator[HERETravelTimeData]
|
||||
update_interval=timedelta(seconds=DEFAULT_SCAN_INTERVAL),
|
||||
)
|
||||
self._api = HERERoutingApi(api_key)
|
||||
self.config = config
|
||||
|
||||
async def _async_update_data(self) -> HERETravelTimeData:
|
||||
"""Get the latest data from the HERE Routing API."""
|
||||
params = prepare_parameters(self.hass, self.config_entry)
|
||||
origin, destination, arrival, departure = prepare_parameters(
|
||||
self.hass, self.config
|
||||
)
|
||||
|
||||
route_mode = (
|
||||
RoutingMode.FAST
|
||||
if self.config.route_mode == ROUTE_MODE_FASTEST
|
||||
else RoutingMode.SHORT
|
||||
)
|
||||
|
||||
_LOGGER.debug(
|
||||
(
|
||||
"Requesting route for origin: %s, destination: %s, route_mode: %s,"
|
||||
" mode: %s, arrival: %s, departure: %s"
|
||||
),
|
||||
params.origin,
|
||||
params.destination,
|
||||
params.route_mode,
|
||||
TransportMode(params.travel_mode),
|
||||
params.arrival,
|
||||
params.departure,
|
||||
origin,
|
||||
destination,
|
||||
route_mode,
|
||||
TransportMode(self.config.travel_mode),
|
||||
arrival,
|
||||
departure,
|
||||
)
|
||||
|
||||
try:
|
||||
response = await self._api.route(
|
||||
transport_mode=TransportMode(params.travel_mode),
|
||||
origin=here_routing.Place(params.origin[0], params.origin[1]),
|
||||
destination=here_routing.Place(
|
||||
params.destination[0], params.destination[1]
|
||||
),
|
||||
routing_mode=params.route_mode,
|
||||
arrival_time=params.arrival,
|
||||
departure_time=params.departure,
|
||||
transport_mode=TransportMode(self.config.travel_mode),
|
||||
origin=here_routing.Place(origin[0], origin[1]),
|
||||
destination=here_routing.Place(destination[0], destination[1]),
|
||||
routing_mode=route_mode,
|
||||
arrival_time=arrival,
|
||||
departure_time=departure,
|
||||
return_values=[Return.POLYINE, Return.SUMMARY],
|
||||
spans=[Spans.NAMES],
|
||||
)
|
||||
@@ -180,7 +175,7 @@ class HERERoutingDataUpdateCoordinator(DataUpdateCoordinator[HERETravelTimeData]
|
||||
class HERETransitDataUpdateCoordinator(
|
||||
DataUpdateCoordinator[HERETravelTimeData | None]
|
||||
):
|
||||
"""HERETravelTime DataUpdateCoordinator for the transit API."""
|
||||
"""HERETravelTime DataUpdateCoordinator."""
|
||||
|
||||
config_entry: HereConfigEntry
|
||||
|
||||
@@ -189,6 +184,7 @@ class HERETransitDataUpdateCoordinator(
|
||||
hass: HomeAssistant,
|
||||
config_entry: HereConfigEntry,
|
||||
api_key: str,
|
||||
config: HERETravelTimeConfig,
|
||||
) -> None:
|
||||
"""Initialize."""
|
||||
super().__init__(
|
||||
@@ -199,31 +195,32 @@ class HERETransitDataUpdateCoordinator(
|
||||
update_interval=timedelta(seconds=DEFAULT_SCAN_INTERVAL),
|
||||
)
|
||||
self._api = HERETransitApi(api_key)
|
||||
self.config = config
|
||||
|
||||
async def _async_update_data(self) -> HERETravelTimeData | None:
|
||||
"""Get the latest data from the HERE Routing API."""
|
||||
params = prepare_parameters(self.hass, self.config_entry)
|
||||
origin, destination, arrival, departure = prepare_parameters(
|
||||
self.hass, self.config
|
||||
)
|
||||
|
||||
_LOGGER.debug(
|
||||
(
|
||||
"Requesting transit route for origin: %s, destination: %s, arrival: %s,"
|
||||
" departure: %s"
|
||||
),
|
||||
params.origin,
|
||||
params.destination,
|
||||
params.arrival,
|
||||
params.departure,
|
||||
origin,
|
||||
destination,
|
||||
arrival,
|
||||
departure,
|
||||
)
|
||||
try:
|
||||
response = await self._api.route(
|
||||
origin=here_transit.Place(
|
||||
latitude=params.origin[0], longitude=params.origin[1]
|
||||
),
|
||||
origin=here_transit.Place(latitude=origin[0], longitude=origin[1]),
|
||||
destination=here_transit.Place(
|
||||
latitude=params.destination[0], longitude=params.destination[1]
|
||||
latitude=destination[0], longitude=destination[1]
|
||||
),
|
||||
arrival_time=params.arrival,
|
||||
departure_time=params.departure,
|
||||
arrival_time=arrival,
|
||||
departure_time=departure,
|
||||
return_values=[
|
||||
here_transit.Return.POLYLINE,
|
||||
here_transit.Return.TRAVEL_SUMMARY,
|
||||
@@ -288,8 +285,8 @@ class HERETransitDataUpdateCoordinator(
|
||||
|
||||
def prepare_parameters(
|
||||
hass: HomeAssistant,
|
||||
config_entry: HereConfigEntry,
|
||||
) -> HERETravelTimeAPIParams:
|
||||
config: HERETravelTimeConfig,
|
||||
) -> tuple[list[str], list[str], str | None, str | None]:
|
||||
"""Prepare parameters for the HERE api."""
|
||||
|
||||
def _from_entity_id(entity_id: str) -> list[str]:
|
||||
@@ -308,55 +305,32 @@ def prepare_parameters(
|
||||
return formatted_coordinates
|
||||
|
||||
# Destination
|
||||
if (
|
||||
destination_entity_id := config_entry.data.get(CONF_DESTINATION_ENTITY_ID)
|
||||
) is not None:
|
||||
destination = _from_entity_id(str(destination_entity_id))
|
||||
if config.destination_entity_id is not None:
|
||||
destination = _from_entity_id(config.destination_entity_id)
|
||||
else:
|
||||
destination = [
|
||||
str(config_entry.data[CONF_DESTINATION_LATITUDE]),
|
||||
str(config_entry.data[CONF_DESTINATION_LONGITUDE]),
|
||||
str(config.destination_latitude),
|
||||
str(config.destination_longitude),
|
||||
]
|
||||
|
||||
# Origin
|
||||
if (origin_entity_id := config_entry.data.get(CONF_ORIGIN_ENTITY_ID)) is not None:
|
||||
origin = _from_entity_id(str(origin_entity_id))
|
||||
if config.origin_entity_id is not None:
|
||||
origin = _from_entity_id(config.origin_entity_id)
|
||||
else:
|
||||
origin = [
|
||||
str(config_entry.data[CONF_ORIGIN_LATITUDE]),
|
||||
str(config_entry.data[CONF_ORIGIN_LONGITUDE]),
|
||||
str(config.origin_latitude),
|
||||
str(config.origin_longitude),
|
||||
]
|
||||
|
||||
# Arrival/Departure
|
||||
arrival: datetime | None = None
|
||||
if (
|
||||
conf_arrival := dt_util.parse_time(
|
||||
config_entry.options.get(CONF_ARRIVAL_TIME, "")
|
||||
)
|
||||
) is not None:
|
||||
arrival = next_datetime(conf_arrival)
|
||||
departure: datetime | None = None
|
||||
if (
|
||||
conf_departure := dt_util.parse_time(
|
||||
config_entry.options.get(CONF_DEPARTURE_TIME, "")
|
||||
)
|
||||
) is not None:
|
||||
departure = next_datetime(conf_departure)
|
||||
arrival: str | None = None
|
||||
departure: str | None = None
|
||||
if config.arrival is not None:
|
||||
arrival = next_datetime(config.arrival).isoformat()
|
||||
if config.departure is not None:
|
||||
departure = next_datetime(config.departure).isoformat()
|
||||
|
||||
route_mode = (
|
||||
RoutingMode.FAST
|
||||
if config_entry.options[CONF_ROUTE_MODE] == ROUTE_MODE_FASTEST
|
||||
else RoutingMode.SHORT
|
||||
)
|
||||
|
||||
return HERETravelTimeAPIParams(
|
||||
destination=destination,
|
||||
origin=origin,
|
||||
travel_mode=config_entry.data[CONF_MODE],
|
||||
route_mode=route_mode,
|
||||
arrival=arrival,
|
||||
departure=departure,
|
||||
)
|
||||
return (origin, destination, arrival, departure)
|
||||
|
||||
|
||||
def build_hass_attribution(sections: list[dict[str, Any]]) -> str | None:
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from datetime import time
|
||||
from typing import TypedDict
|
||||
|
||||
|
||||
@@ -21,12 +21,16 @@ class HERETravelTimeData(TypedDict):
|
||||
|
||||
|
||||
@dataclass
|
||||
class HERETravelTimeAPIParams:
|
||||
"""Configuration for polling the HERE API."""
|
||||
class HERETravelTimeConfig:
|
||||
"""Configuration for HereTravelTimeDataUpdateCoordinator."""
|
||||
|
||||
destination: list[str]
|
||||
origin: list[str]
|
||||
destination_latitude: float | None
|
||||
destination_longitude: float | None
|
||||
destination_entity_id: str | None
|
||||
origin_latitude: float | None
|
||||
origin_longitude: float | None
|
||||
origin_entity_id: str | None
|
||||
travel_mode: str
|
||||
route_mode: str
|
||||
arrival: datetime | None
|
||||
departure: datetime | None
|
||||
arrival: time | None
|
||||
departure: time | None
|
||||
|
||||
@@ -4,10 +4,8 @@ import asyncio
|
||||
from collections.abc import Callable, Coroutine
|
||||
import itertools as it
|
||||
import logging
|
||||
import struct
|
||||
from typing import Any
|
||||
|
||||
import aiofiles
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import config as conf_util, core_config
|
||||
@@ -96,17 +94,6 @@ DEPRECATION_URL = (
|
||||
)
|
||||
|
||||
|
||||
def _is_32_bit() -> bool:
|
||||
size = struct.calcsize("P")
|
||||
return size * 8 == 32
|
||||
|
||||
|
||||
async def _get_arch() -> str:
|
||||
async with aiofiles.open("/etc/apk/arch") as arch_file:
|
||||
raw_arch = (await arch_file.read()).strip()
|
||||
return {"x86": "i386", "x86_64": "amd64"}.get(raw_arch, raw_arch)
|
||||
|
||||
|
||||
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa: C901
|
||||
"""Set up general services related to Home Assistant."""
|
||||
|
||||
@@ -416,21 +403,23 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa:
|
||||
installation_type = info["installation_type"][15:]
|
||||
if installation_type in {"Core", "Container"}:
|
||||
deprecated_method = installation_type == "Core"
|
||||
bit32 = _is_32_bit()
|
||||
arch = info["arch"]
|
||||
if bit32 and installation_type == "Container":
|
||||
arch = await _get_arch()
|
||||
if arch == "armv7" and installation_type == "Container":
|
||||
ir.async_create_issue(
|
||||
hass,
|
||||
DOMAIN,
|
||||
"deprecated_container",
|
||||
"deprecated_container_armv7",
|
||||
breaks_in_ha_version="2025.12.0",
|
||||
learn_more_url=DEPRECATION_URL,
|
||||
is_fixable=False,
|
||||
severity=IssueSeverity.WARNING,
|
||||
translation_key="deprecated_container",
|
||||
translation_placeholders={"arch": arch},
|
||||
translation_key="deprecated_container_armv7",
|
||||
)
|
||||
deprecated_architecture = bit32 and installation_type != "Container"
|
||||
deprecated_architecture = False
|
||||
if arch in {"i386", "armhf"} or (
|
||||
arch == "armv7" and installation_type != "Container"
|
||||
):
|
||||
deprecated_architecture = True
|
||||
if deprecated_method or deprecated_architecture:
|
||||
issue_id = "deprecated"
|
||||
if deprecated_method:
|
||||
@@ -441,6 +430,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: # noqa:
|
||||
hass,
|
||||
DOMAIN,
|
||||
issue_id,
|
||||
breaks_in_ha_version="2025.12.0",
|
||||
learn_more_url=DEPRECATION_URL,
|
||||
is_fixable=False,
|
||||
severity=IssueSeverity.WARNING,
|
||||
|
||||
@@ -107,9 +107,9 @@
|
||||
"title": "Deprecation notice: 32-bit architecture",
|
||||
"description": "This system uses 32-bit hardware (`{arch}`), which has been deprecated and will no longer receive updates after the release of Home Assistant 2025.12. As your hardware is no longer capable of running newer versions of Home Assistant, you will need to migrate to new hardware."
|
||||
},
|
||||
"deprecated_container": {
|
||||
"deprecated_container_armv7": {
|
||||
"title": "[%key:component::homeassistant::issues::deprecated_architecture::title%]",
|
||||
"description": "This system is running on a 32-bit operating system (`{arch}`), which has been deprecated and will no longer receive updates after the release of Home Assistant 2025.12. Check if your system is capable of running a 64-bit operating system. If not, you will need to migrate to new hardware."
|
||||
"description": "This system is running on a 32-bit operating system (`armv7`), which has been deprecated and will no longer receive updates after the release of Home Assistant 2025.12. Check if your system is capable of running a 64-bit operating system. If not, you will need to migrate to new hardware."
|
||||
},
|
||||
"deprecated_os_aarch64": {
|
||||
"title": "[%key:component::homeassistant::issues::deprecated_architecture::title%]",
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/hyperion",
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["hyperion"],
|
||||
"requirements": ["hyperion-py==0.7.6"],
|
||||
"requirements": ["hyperion-py==0.7.5"],
|
||||
"ssdp": [
|
||||
{
|
||||
"manufacturer": "Hyperion Open Source Ambient Lighting",
|
||||
|
||||
@@ -82,9 +82,6 @@
|
||||
},
|
||||
"usb_capture": {
|
||||
"name": "Component USB capture"
|
||||
},
|
||||
"audio_capture": {
|
||||
"name": "Component Audio capture"
|
||||
}
|
||||
},
|
||||
"sensor": {
|
||||
|
||||
@@ -9,7 +9,6 @@ from hyperion import client
|
||||
from hyperion.const import (
|
||||
KEY_COMPONENT,
|
||||
KEY_COMPONENTID_ALL,
|
||||
KEY_COMPONENTID_AUDIO,
|
||||
KEY_COMPONENTID_BLACKBORDER,
|
||||
KEY_COMPONENTID_BOBLIGHTSERVER,
|
||||
KEY_COMPONENTID_FORWARDER,
|
||||
@@ -60,7 +59,6 @@ COMPONENT_SWITCHES = [
|
||||
KEY_COMPONENTID_GRABBER,
|
||||
KEY_COMPONENTID_LEDDEVICE,
|
||||
KEY_COMPONENTID_V4L,
|
||||
KEY_COMPONENTID_AUDIO,
|
||||
]
|
||||
|
||||
|
||||
@@ -85,7 +83,6 @@ def _component_to_translation_key(component: str) -> str:
|
||||
KEY_COMPONENTID_GRABBER: "platform_capture",
|
||||
KEY_COMPONENTID_LEDDEVICE: "led_device",
|
||||
KEY_COMPONENTID_V4L: "usb_capture",
|
||||
KEY_COMPONENTID_AUDIO: "audio_capture",
|
||||
}[component]
|
||||
|
||||
|
||||
|
||||
@@ -30,6 +30,7 @@ from homeassistant.helpers.selector import (
|
||||
TextSelectorConfig,
|
||||
TextSelectorType,
|
||||
)
|
||||
from homeassistant.helpers.service_info.hassio import HassioServiceInfo
|
||||
|
||||
from .const import DEFAULT_VERIFY_SSL, DOMAIN
|
||||
|
||||
@@ -81,6 +82,7 @@ class ImmichConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
|
||||
_name: str
|
||||
_current_data: Mapping[str, Any]
|
||||
_hassio_discovery: dict[str, Any] | None = None
|
||||
|
||||
async def async_step_user(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
@@ -172,3 +174,66 @@ class ImmichConfigFlow(ConfigFlow, domain=DOMAIN):
|
||||
description_placeholders={"name": self._name},
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
async def async_step_hassio(
|
||||
self, discovery_info: HassioServiceInfo
|
||||
) -> ConfigFlowResult:
|
||||
"""Handle the discovery step via hassio."""
|
||||
self._hassio_discovery = discovery_info.config
|
||||
return await self.async_step_hassio_confirm()
|
||||
|
||||
async def async_step_hassio_confirm(
|
||||
self, user_input: dict[str, Any] | None = None
|
||||
) -> ConfigFlowResult:
|
||||
"""Confirm Supervisor discovery."""
|
||||
assert self._hassio_discovery
|
||||
host = self._hassio_discovery[CONF_HOST]
|
||||
port = self._hassio_discovery[CONF_PORT]
|
||||
ssl = self._hassio_discovery[CONF_SSL]
|
||||
addon = self._hassio_discovery["addon"]
|
||||
|
||||
errors: dict[str, str] = {}
|
||||
if user_input is not None:
|
||||
try:
|
||||
my_user_info = await check_user_info(
|
||||
self.hass,
|
||||
host,
|
||||
port,
|
||||
ssl,
|
||||
user_input[CONF_VERIFY_SSL],
|
||||
user_input[CONF_API_KEY],
|
||||
)
|
||||
except ImmichUnauthorizedError:
|
||||
errors["base"] = "invalid_auth"
|
||||
except CONNECT_ERRORS:
|
||||
errors["base"] = "cannot_connect"
|
||||
except Exception:
|
||||
_LOGGER.exception("Unexpected exception")
|
||||
errors["base"] = "unknown"
|
||||
else:
|
||||
await self.async_set_unique_id(my_user_info.user_id)
|
||||
self._abort_if_unique_id_configured()
|
||||
return self.async_create_entry(
|
||||
title=my_user_info.name,
|
||||
data={
|
||||
CONF_HOST: host,
|
||||
CONF_PORT: port,
|
||||
CONF_SSL: ssl,
|
||||
CONF_VERIFY_SSL: user_input[CONF_VERIFY_SSL],
|
||||
CONF_API_KEY: user_input[CONF_API_KEY],
|
||||
},
|
||||
)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="hassio_confirm",
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Required(CONF_API_KEY): TextSelector(
|
||||
config=TextSelectorConfig(type=TextSelectorType.PASSWORD)
|
||||
),
|
||||
vol.Required(CONF_VERIFY_SSL, default=DEFAULT_VERIFY_SSL): bool,
|
||||
}
|
||||
),
|
||||
description_placeholders={"addon": addon},
|
||||
errors=errors,
|
||||
)
|
||||
|
||||
@@ -26,6 +26,17 @@
|
||||
"data_description": {
|
||||
"api_key": "[%key:component::immich::common::data_desc_api_key%]"
|
||||
}
|
||||
},
|
||||
"hassio_confirm": {
|
||||
"description": "Setup connection to add-on {addon}",
|
||||
"data": {
|
||||
"api_key": "[%key:common::config_flow::data::api_key%]",
|
||||
"verify_ssl": "[%key:common::config_flow::data::verify_ssl%]"
|
||||
},
|
||||
"data_description": {
|
||||
"api_key": "[%key:component::immich::common::data_desc_api_key%]",
|
||||
"verify_ssl": "[%key:component::immich::common::data_desc_ssl_verify%]"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
|
||||
@@ -6,6 +6,6 @@
|
||||
"documentation": "https://www.home-assistant.io/integrations/jewish_calendar",
|
||||
"iot_class": "calculated",
|
||||
"loggers": ["hdate"],
|
||||
"requirements": ["hdate[astral]==1.1.1"],
|
||||
"requirements": ["hdate[astral]==1.1.0"],
|
||||
"single_config_entry": true
|
||||
}
|
||||
|
||||
@@ -24,11 +24,3 @@ async def async_get_auth_implementation(
|
||||
token_url=OAUTH2_TOKEN,
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
async def async_get_description_placeholders(hass: HomeAssistant) -> dict[str, str]:
|
||||
"""Return description placeholders for the credentials dialog."""
|
||||
return {
|
||||
"developer_dashboard_url": "https://developer.honeywellhome.com",
|
||||
"redirect_url": "https://my.home-assistant.io/redirect/oauth",
|
||||
}
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
{
|
||||
"application_credentials": {
|
||||
"description": "To be able to log in to Honeywell Lyric the integration requires a client ID and secret. To acquire those, please follow the following steps.\n\n1. Go to the [Honeywell Lyric Developer Apps Dashboard]({developer_dashboard_url}).\n1. Sign up for a developer account if you don't have one yet. This is a separate account from your Honeywell account.\n1. Log in with your Honeywell Lyric developer account.\n1. Go to the **My Apps** section.\n1. Press the **CREATE NEW APP** button.\n1. Give the application a name of your choice.\n1. Set the **Callback URL** to `{redirect_url}`.\n1. Save your changes.\\n1. Copy the **Consumer Key** and paste it here as the **Client ID**, then copy the **Consumer Secret** and paste it here as the **Client Secret**."
|
||||
},
|
||||
"config": {
|
||||
"step": {
|
||||
"pick_implementation": {
|
||||
@@ -12,7 +9,7 @@
|
||||
"description": "The Lyric integration needs to re-authenticate your account."
|
||||
},
|
||||
"oauth_discovery": {
|
||||
"description": "Home Assistant has found a Honeywell Lyric device on your network. Be aware that the setup of the Lyric integration is more complicated than other integrations. Press **Submit** to continue setting up Honeywell Lyric."
|
||||
"description": "Home Assistant has found a Honeywell Lyric device on your network. Press **Submit** to continue setting up Honeywell Lyric."
|
||||
}
|
||||
},
|
||||
"abort": {
|
||||
|
||||
@@ -8,6 +8,6 @@
|
||||
"iot_class": "calculated",
|
||||
"loggers": ["yt_dlp"],
|
||||
"quality_scale": "internal",
|
||||
"requirements": ["yt-dlp[default]==2025.06.09"],
|
||||
"requirements": ["yt-dlp[default]==2025.05.22"],
|
||||
"single_config_entry": true
|
||||
}
|
||||
|
||||
@@ -45,7 +45,7 @@ from .const import ( # noqa: F401
|
||||
SupportedDialect,
|
||||
)
|
||||
from .core import Recorder
|
||||
from .services import async_setup_services
|
||||
from .services import async_register_services
|
||||
from .tasks import AddRecorderPlatformTask
|
||||
from .util import get_instance
|
||||
|
||||
@@ -174,7 +174,7 @@ async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
|
||||
instance.async_initialize()
|
||||
instance.async_register()
|
||||
instance.start()
|
||||
async_setup_services(hass)
|
||||
async_register_services(hass, instance)
|
||||
websocket_api.async_setup(hass)
|
||||
|
||||
await _async_setup_integration_platform(hass, instance)
|
||||
|
||||
@@ -17,7 +17,6 @@ from homeassistant.core import (
|
||||
)
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
from homeassistant.helpers.entityfilter import generate_filter
|
||||
from homeassistant.helpers.recorder import DATA_INSTANCE
|
||||
from homeassistant.helpers.service import (
|
||||
async_extract_entity_ids,
|
||||
async_register_admin_service,
|
||||
@@ -26,6 +25,7 @@ from homeassistant.util import dt as dt_util
|
||||
from homeassistant.util.json import JsonArrayType, JsonObjectType
|
||||
|
||||
from .const import ATTR_APPLY_FILTER, ATTR_KEEP_DAYS, ATTR_REPACK, DOMAIN
|
||||
from .core import Recorder
|
||||
from .statistics import statistics_during_period
|
||||
from .tasks import PurgeEntitiesTask, PurgeTask
|
||||
|
||||
@@ -87,137 +87,155 @@ SERVICE_GET_STATISTICS_SCHEMA = vol.Schema(
|
||||
)
|
||||
|
||||
|
||||
async def _async_handle_purge_service(service: ServiceCall) -> None:
|
||||
"""Handle calls to the purge service."""
|
||||
hass = service.hass
|
||||
instance = hass.data[DATA_INSTANCE]
|
||||
kwargs = service.data
|
||||
keep_days = kwargs.get(ATTR_KEEP_DAYS, instance.keep_days)
|
||||
repack = cast(bool, kwargs[ATTR_REPACK])
|
||||
apply_filter = cast(bool, kwargs[ATTR_APPLY_FILTER])
|
||||
purge_before = dt_util.utcnow() - timedelta(days=keep_days)
|
||||
instance.queue_task(PurgeTask(purge_before, repack, apply_filter))
|
||||
|
||||
|
||||
async def _async_handle_purge_entities_service(service: ServiceCall) -> None:
|
||||
"""Handle calls to the purge entities service."""
|
||||
hass = service.hass
|
||||
entity_ids = await async_extract_entity_ids(hass, service)
|
||||
domains = service.data.get(ATTR_DOMAINS, [])
|
||||
keep_days = service.data.get(ATTR_KEEP_DAYS, 0)
|
||||
entity_globs = service.data.get(ATTR_ENTITY_GLOBS, [])
|
||||
entity_filter = generate_filter(domains, list(entity_ids), [], [], entity_globs)
|
||||
purge_before = dt_util.utcnow() - timedelta(days=keep_days)
|
||||
hass.data[DATA_INSTANCE].queue_task(PurgeEntitiesTask(entity_filter, purge_before))
|
||||
|
||||
|
||||
async def _async_handle_enable_service(service: ServiceCall) -> None:
|
||||
service.hass.data[DATA_INSTANCE].set_enable(True)
|
||||
|
||||
|
||||
async def _async_handle_disable_service(service: ServiceCall) -> None:
|
||||
service.hass.data[DATA_INSTANCE].set_enable(False)
|
||||
|
||||
|
||||
async def _async_handle_get_statistics_service(
|
||||
service: ServiceCall,
|
||||
) -> ServiceResponse:
|
||||
"""Handle calls to the get_statistics service."""
|
||||
hass = service.hass
|
||||
start_time = dt_util.as_utc(service.data["start_time"])
|
||||
end_time = (
|
||||
dt_util.as_utc(service.data["end_time"]) if "end_time" in service.data else None
|
||||
)
|
||||
|
||||
statistic_ids = service.data["statistic_ids"]
|
||||
types = service.data["types"]
|
||||
period = service.data["period"]
|
||||
units = service.data.get("units")
|
||||
|
||||
result = await hass.data[DATA_INSTANCE].async_add_executor_job(
|
||||
statistics_during_period,
|
||||
hass,
|
||||
start_time,
|
||||
end_time,
|
||||
statistic_ids,
|
||||
period,
|
||||
units,
|
||||
types,
|
||||
)
|
||||
|
||||
formatted_result: JsonObjectType = {}
|
||||
for statistic_id, statistic_rows in result.items():
|
||||
formatted_statistic_rows: JsonArrayType = []
|
||||
|
||||
for row in statistic_rows:
|
||||
formatted_row: JsonObjectType = {
|
||||
"start": dt_util.utc_from_timestamp(row["start"]).isoformat(),
|
||||
"end": dt_util.utc_from_timestamp(row["end"]).isoformat(),
|
||||
}
|
||||
if (last_reset := row.get("last_reset")) is not None:
|
||||
formatted_row["last_reset"] = dt_util.utc_from_timestamp(
|
||||
last_reset
|
||||
).isoformat()
|
||||
if (state := row.get("state")) is not None:
|
||||
formatted_row["state"] = state
|
||||
if (sum_value := row.get("sum")) is not None:
|
||||
formatted_row["sum"] = sum_value
|
||||
if (min_value := row.get("min")) is not None:
|
||||
formatted_row["min"] = min_value
|
||||
if (max_value := row.get("max")) is not None:
|
||||
formatted_row["max"] = max_value
|
||||
if (mean := row.get("mean")) is not None:
|
||||
formatted_row["mean"] = mean
|
||||
if (change := row.get("change")) is not None:
|
||||
formatted_row["change"] = change
|
||||
|
||||
formatted_statistic_rows.append(formatted_row)
|
||||
|
||||
formatted_result[statistic_id] = formatted_statistic_rows
|
||||
|
||||
return {"statistics": formatted_result}
|
||||
|
||||
|
||||
@callback
|
||||
def async_setup_services(hass: HomeAssistant) -> None:
|
||||
"""Register recorder services."""
|
||||
def _async_register_purge_service(hass: HomeAssistant, instance: Recorder) -> None:
|
||||
async def async_handle_purge_service(service: ServiceCall) -> None:
|
||||
"""Handle calls to the purge service."""
|
||||
kwargs = service.data
|
||||
keep_days = kwargs.get(ATTR_KEEP_DAYS, instance.keep_days)
|
||||
repack = cast(bool, kwargs[ATTR_REPACK])
|
||||
apply_filter = cast(bool, kwargs[ATTR_APPLY_FILTER])
|
||||
purge_before = dt_util.utcnow() - timedelta(days=keep_days)
|
||||
instance.queue_task(PurgeTask(purge_before, repack, apply_filter))
|
||||
|
||||
async_register_admin_service(
|
||||
hass,
|
||||
DOMAIN,
|
||||
SERVICE_PURGE,
|
||||
_async_handle_purge_service,
|
||||
async_handle_purge_service,
|
||||
schema=SERVICE_PURGE_SCHEMA,
|
||||
)
|
||||
|
||||
|
||||
@callback
|
||||
def _async_register_purge_entities_service(
|
||||
hass: HomeAssistant, instance: Recorder
|
||||
) -> None:
|
||||
async def async_handle_purge_entities_service(service: ServiceCall) -> None:
|
||||
"""Handle calls to the purge entities service."""
|
||||
entity_ids = await async_extract_entity_ids(hass, service)
|
||||
domains = service.data.get(ATTR_DOMAINS, [])
|
||||
keep_days = service.data.get(ATTR_KEEP_DAYS, 0)
|
||||
entity_globs = service.data.get(ATTR_ENTITY_GLOBS, [])
|
||||
entity_filter = generate_filter(domains, list(entity_ids), [], [], entity_globs)
|
||||
purge_before = dt_util.utcnow() - timedelta(days=keep_days)
|
||||
instance.queue_task(PurgeEntitiesTask(entity_filter, purge_before))
|
||||
|
||||
async_register_admin_service(
|
||||
hass,
|
||||
DOMAIN,
|
||||
SERVICE_PURGE_ENTITIES,
|
||||
_async_handle_purge_entities_service,
|
||||
async_handle_purge_entities_service,
|
||||
schema=SERVICE_PURGE_ENTITIES_SCHEMA,
|
||||
)
|
||||
|
||||
|
||||
@callback
|
||||
def _async_register_enable_service(hass: HomeAssistant, instance: Recorder) -> None:
|
||||
async def async_handle_enable_service(service: ServiceCall) -> None:
|
||||
instance.set_enable(True)
|
||||
|
||||
async_register_admin_service(
|
||||
hass,
|
||||
DOMAIN,
|
||||
SERVICE_ENABLE,
|
||||
_async_handle_enable_service,
|
||||
async_handle_enable_service,
|
||||
schema=SERVICE_ENABLE_SCHEMA,
|
||||
)
|
||||
|
||||
|
||||
@callback
|
||||
def _async_register_disable_service(hass: HomeAssistant, instance: Recorder) -> None:
|
||||
async def async_handle_disable_service(service: ServiceCall) -> None:
|
||||
instance.set_enable(False)
|
||||
|
||||
async_register_admin_service(
|
||||
hass,
|
||||
DOMAIN,
|
||||
SERVICE_DISABLE,
|
||||
_async_handle_disable_service,
|
||||
async_handle_disable_service,
|
||||
schema=SERVICE_DISABLE_SCHEMA,
|
||||
)
|
||||
|
||||
|
||||
@callback
|
||||
def _async_register_get_statistics_service(
|
||||
hass: HomeAssistant, instance: Recorder
|
||||
) -> None:
|
||||
async def async_handle_get_statistics_service(
|
||||
service: ServiceCall,
|
||||
) -> ServiceResponse:
|
||||
"""Handle calls to the get_statistics service."""
|
||||
start_time = dt_util.as_utc(service.data["start_time"])
|
||||
end_time = (
|
||||
dt_util.as_utc(service.data["end_time"])
|
||||
if "end_time" in service.data
|
||||
else None
|
||||
)
|
||||
|
||||
statistic_ids = service.data["statistic_ids"]
|
||||
types = service.data["types"]
|
||||
period = service.data["period"]
|
||||
units = service.data.get("units")
|
||||
|
||||
result = await instance.async_add_executor_job(
|
||||
statistics_during_period,
|
||||
hass,
|
||||
start_time,
|
||||
end_time,
|
||||
statistic_ids,
|
||||
period,
|
||||
units,
|
||||
types,
|
||||
)
|
||||
|
||||
formatted_result: JsonObjectType = {}
|
||||
for statistic_id, statistic_rows in result.items():
|
||||
formatted_statistic_rows: JsonArrayType = []
|
||||
|
||||
for row in statistic_rows:
|
||||
formatted_row: JsonObjectType = {
|
||||
"start": dt_util.utc_from_timestamp(row["start"]).isoformat(),
|
||||
"end": dt_util.utc_from_timestamp(row["end"]).isoformat(),
|
||||
}
|
||||
if (last_reset := row.get("last_reset")) is not None:
|
||||
formatted_row["last_reset"] = dt_util.utc_from_timestamp(
|
||||
last_reset
|
||||
).isoformat()
|
||||
if (state := row.get("state")) is not None:
|
||||
formatted_row["state"] = state
|
||||
if (sum_value := row.get("sum")) is not None:
|
||||
formatted_row["sum"] = sum_value
|
||||
if (min_value := row.get("min")) is not None:
|
||||
formatted_row["min"] = min_value
|
||||
if (max_value := row.get("max")) is not None:
|
||||
formatted_row["max"] = max_value
|
||||
if (mean := row.get("mean")) is not None:
|
||||
formatted_row["mean"] = mean
|
||||
if (change := row.get("change")) is not None:
|
||||
formatted_row["change"] = change
|
||||
|
||||
formatted_statistic_rows.append(formatted_row)
|
||||
|
||||
formatted_result[statistic_id] = formatted_statistic_rows
|
||||
|
||||
return {"statistics": formatted_result}
|
||||
|
||||
async_register_admin_service(
|
||||
hass,
|
||||
DOMAIN,
|
||||
SERVICE_GET_STATISTICS,
|
||||
_async_handle_get_statistics_service,
|
||||
async_handle_get_statistics_service,
|
||||
schema=SERVICE_GET_STATISTICS_SCHEMA,
|
||||
supports_response=SupportsResponse.ONLY,
|
||||
)
|
||||
|
||||
|
||||
@callback
|
||||
def async_register_services(hass: HomeAssistant, instance: Recorder) -> None:
|
||||
"""Register recorder services."""
|
||||
_async_register_purge_service(hass, instance)
|
||||
_async_register_purge_entities_service(hass, instance)
|
||||
_async_register_enable_service(hass, instance)
|
||||
_async_register_disable_service(hass, instance)
|
||||
_async_register_get_statistics_service(hass, instance)
|
||||
|
||||
@@ -31,7 +31,7 @@ async def async_setup_entry(
|
||||
if dev_id in event_entities:
|
||||
return
|
||||
# new player!
|
||||
event_entity = RoonEventEntity(roon_server, player_data, config_entry.entry_id)
|
||||
event_entity = RoonEventEntity(roon_server, player_data)
|
||||
event_entities.add(dev_id)
|
||||
async_add_entities([event_entity])
|
||||
|
||||
@@ -50,14 +50,13 @@ class RoonEventEntity(EventEntity):
|
||||
_attr_event_types = ["volume_up", "volume_down", "mute_toggle"]
|
||||
_attr_translation_key = "volume"
|
||||
|
||||
def __init__(self, server, player_data, entry_id):
|
||||
def __init__(self, server, player_data):
|
||||
"""Initialize the entity."""
|
||||
self._server = server
|
||||
self._player_data = player_data
|
||||
player_name = player_data["display_name"]
|
||||
self._attr_name = f"{player_name} roon volume"
|
||||
self._attr_unique_id = self._player_data["dev_id"]
|
||||
self._entry_id = entry_id
|
||||
|
||||
if self._player_data.get("source_controls"):
|
||||
dev_model = self._player_data["source_controls"][0].get("display_name")
|
||||
@@ -70,7 +69,7 @@ class RoonEventEntity(EventEntity):
|
||||
name=cast(str | None, self.name),
|
||||
manufacturer="RoonLabs",
|
||||
model=dev_model,
|
||||
via_device=(DOMAIN, self._entry_id),
|
||||
via_device=(DOMAIN, self._server.roon_id),
|
||||
)
|
||||
|
||||
def _roonapi_volume_callback(
|
||||
|
||||
@@ -72,7 +72,7 @@ async def async_setup_entry(
|
||||
dev_id = player_data["dev_id"]
|
||||
if dev_id not in media_players:
|
||||
# new player!
|
||||
media_player = RoonDevice(roon_server, player_data, config_entry.entry_id)
|
||||
media_player = RoonDevice(roon_server, player_data)
|
||||
media_players.add(dev_id)
|
||||
async_add_entities([media_player])
|
||||
else:
|
||||
@@ -106,7 +106,7 @@ class RoonDevice(MediaPlayerEntity):
|
||||
| MediaPlayerEntityFeature.PLAY_MEDIA
|
||||
)
|
||||
|
||||
def __init__(self, server, player_data, entry_id):
|
||||
def __init__(self, server, player_data):
|
||||
"""Initialize Roon device object."""
|
||||
self._remove_signal_status = None
|
||||
self._server = server
|
||||
@@ -125,7 +125,6 @@ class RoonDevice(MediaPlayerEntity):
|
||||
self._attr_volume_level = 0
|
||||
self._volume_fixed = True
|
||||
self._volume_incremental = False
|
||||
self._entry_id = entry_id
|
||||
self.update_data(player_data)
|
||||
|
||||
async def async_added_to_hass(self) -> None:
|
||||
@@ -167,7 +166,7 @@ class RoonDevice(MediaPlayerEntity):
|
||||
name=cast(str | None, self.name),
|
||||
manufacturer="RoonLabs",
|
||||
model=dev_model,
|
||||
via_device=(DOMAIN, self._entry_id),
|
||||
via_device=(DOMAIN, self._server.roon_id),
|
||||
)
|
||||
|
||||
def update_data(self, player_data=None):
|
||||
|
||||
@@ -64,7 +64,6 @@ from .utils import (
|
||||
get_http_port,
|
||||
get_rpc_scripts_event_types,
|
||||
get_ws_context,
|
||||
remove_stale_blu_trv_devices,
|
||||
)
|
||||
|
||||
PLATFORMS: Final = [
|
||||
@@ -301,7 +300,6 @@ async def _async_setup_rpc_entry(hass: HomeAssistant, entry: ShellyConfigEntry)
|
||||
runtime_data.rpc_script_events = await get_rpc_scripts_event_types(
|
||||
device, ignore_scripts=[BLE_SCRIPT_NAME]
|
||||
)
|
||||
remove_stale_blu_trv_devices(hass, device, entry)
|
||||
except (DeviceConnectionError, MacAddressMismatchError, RpcCallError) as err:
|
||||
await device.shutdown()
|
||||
raise ConfigEntryNotReady(
|
||||
|
||||
@@ -61,8 +61,8 @@ rules:
|
||||
reconfiguration-flow: done
|
||||
repair-issues: done
|
||||
stale-devices:
|
||||
status: done
|
||||
comment: BLU TRV is removed when un-paired
|
||||
status: todo
|
||||
comment: BLU TRV needs to be removed when un-paired
|
||||
|
||||
# Platinum
|
||||
async-dependency: done
|
||||
|
||||
@@ -16,7 +16,6 @@ from aioshelly.const import (
|
||||
DEFAULT_COAP_PORT,
|
||||
DEFAULT_HTTP_PORT,
|
||||
MODEL_1L,
|
||||
MODEL_BLU_GATEWAY_G3,
|
||||
MODEL_DIMMER,
|
||||
MODEL_DIMMER_2,
|
||||
MODEL_EM3,
|
||||
@@ -822,32 +821,3 @@ def get_block_device_info(
|
||||
manufacturer="Shelly",
|
||||
via_device=(DOMAIN, mac),
|
||||
)
|
||||
|
||||
|
||||
@callback
|
||||
def remove_stale_blu_trv_devices(
|
||||
hass: HomeAssistant, rpc_device: RpcDevice, entry: ConfigEntry
|
||||
) -> None:
|
||||
"""Remove stale BLU TRV devices."""
|
||||
if rpc_device.model != MODEL_BLU_GATEWAY_G3:
|
||||
return
|
||||
|
||||
dev_reg = dr.async_get(hass)
|
||||
devices = dev_reg.devices.get_devices_for_config_entry_id(entry.entry_id)
|
||||
config = rpc_device.config
|
||||
blutrv_keys = get_rpc_key_ids(config, BLU_TRV_IDENTIFIER)
|
||||
trv_addrs = [config[f"{BLU_TRV_IDENTIFIER}:{key}"]["addr"] for key in blutrv_keys]
|
||||
|
||||
for device in devices:
|
||||
if not device.via_device_id:
|
||||
# Device is not a sub-device, skip
|
||||
continue
|
||||
|
||||
if any(
|
||||
identifier[0] == DOMAIN and identifier[1] in trv_addrs
|
||||
for identifier in device.identifiers
|
||||
):
|
||||
continue
|
||||
|
||||
LOGGER.debug("Removing stale BLU TRV device %s", device.name)
|
||||
dev_reg.async_update_device(device.id, remove_config_entry_id=entry.entry_id)
|
||||
|
||||
@@ -153,6 +153,11 @@ class SqueezeboxButtonEntity(SqueezeboxEntity, ButtonEntity):
|
||||
f"{format_mac(self._player.player_id)}_{entity_description.key}"
|
||||
)
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return True if entity is available."""
|
||||
return self.coordinator.available and super().available
|
||||
|
||||
async def async_press(self) -> None:
|
||||
"""Execute the button action."""
|
||||
await self._player.async_query("button", self.entity_description.press_action)
|
||||
|
||||
@@ -33,13 +33,6 @@ class SqueezeboxEntity(CoordinatorEntity[SqueezeBoxPlayerUpdateCoordinator]):
|
||||
manufacturer=self._player.creator,
|
||||
)
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return True if entity is available."""
|
||||
# super().available refers to CoordinatorEntity.available (self.coordinator.last_update_success)
|
||||
# self.coordinator.available is the custom availability flag from SqueezeBoxPlayerUpdateCoordinator
|
||||
return self.coordinator.available and super().available
|
||||
|
||||
|
||||
class LMSStatusEntity(CoordinatorEntity[LMSStatusDataUpdateCoordinator]):
|
||||
"""Defines a base status sensor entity."""
|
||||
|
||||
@@ -246,6 +246,11 @@ class SqueezeBoxMediaPlayerEntity(SqueezeboxEntity, MediaPlayerEntity):
|
||||
CONF_BROWSE_LIMIT, DEFAULT_BROWSE_LIMIT
|
||||
)
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
"""Return True if entity is available."""
|
||||
return self.coordinator.available and super().available
|
||||
|
||||
@property
|
||||
def extra_state_attributes(self) -> dict[str, Any]:
|
||||
"""Return device-specific attributes."""
|
||||
|
||||
@@ -233,13 +233,13 @@ class TelegramNotificationService:
|
||||
"""Initialize the service."""
|
||||
self.app = app
|
||||
self.config = config
|
||||
self._parsers: dict[str, str | None] = {
|
||||
self._parsers = {
|
||||
PARSER_HTML: ParseMode.HTML,
|
||||
PARSER_MD: ParseMode.MARKDOWN,
|
||||
PARSER_MD2: ParseMode.MARKDOWN_V2,
|
||||
PARSER_PLAIN_TEXT: None,
|
||||
}
|
||||
self.parse_mode = self._parsers[parser]
|
||||
self.parse_mode = self._parsers.get(parser)
|
||||
self.bot = bot
|
||||
self.hass = hass
|
||||
self._last_message_id: dict[int, int] = {}
|
||||
|
||||
@@ -54,7 +54,6 @@ from .const import (
|
||||
PARSER_HTML,
|
||||
PARSER_MD,
|
||||
PARSER_MD2,
|
||||
PARSER_PLAIN_TEXT,
|
||||
PLATFORM_BROADCAST,
|
||||
PLATFORM_POLLING,
|
||||
PLATFORM_WEBHOOKS,
|
||||
@@ -127,8 +126,8 @@ OPTIONS_SCHEMA: vol.Schema = vol.Schema(
|
||||
ATTR_PARSER,
|
||||
): SelectSelector(
|
||||
SelectSelectorConfig(
|
||||
options=[PARSER_MD, PARSER_MD2, PARSER_HTML, PARSER_PLAIN_TEXT],
|
||||
translation_key="parse_mode",
|
||||
options=[PARSER_MD, PARSER_MD2, PARSER_HTML],
|
||||
translation_key="parsers",
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -144,8 +143,6 @@ class OptionsFlowHandler(OptionsFlow):
|
||||
"""Manage the options."""
|
||||
|
||||
if user_input is not None:
|
||||
if user_input[ATTR_PARSER] == PARSER_PLAIN_TEXT:
|
||||
user_input[ATTR_PARSER] = None
|
||||
return self.async_create_entry(data=user_input)
|
||||
|
||||
return self.async_show_form(
|
||||
|
||||
@@ -27,7 +27,6 @@ send_message:
|
||||
- "markdown"
|
||||
- "markdownv2"
|
||||
- "plain_text"
|
||||
translation_key: "parse_mode"
|
||||
disable_notification:
|
||||
selector:
|
||||
boolean:
|
||||
|
||||
@@ -106,12 +106,11 @@
|
||||
"webhooks": "Webhooks"
|
||||
}
|
||||
},
|
||||
"parse_mode": {
|
||||
"parsers": {
|
||||
"options": {
|
||||
"markdown": "Markdown (Legacy)",
|
||||
"markdownv2": "MarkdownV2",
|
||||
"html": "HTML",
|
||||
"plain_text": "Plain text"
|
||||
"html": "HTML"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
Generated
+1
-1
@@ -47,7 +47,7 @@ FLOWS = {
|
||||
"airzone",
|
||||
"airzone_cloud",
|
||||
"alarmdecoder",
|
||||
"alexa_devices",
|
||||
"amazon_devices",
|
||||
"amberelectric",
|
||||
"ambient_network",
|
||||
"ambient_station",
|
||||
|
||||
@@ -207,11 +207,11 @@
|
||||
"amazon": {
|
||||
"name": "Amazon",
|
||||
"integrations": {
|
||||
"alexa_devices": {
|
||||
"amazon_devices": {
|
||||
"integration_type": "hub",
|
||||
"config_flow": true,
|
||||
"iot_class": "cloud_polling",
|
||||
"name": "Alexa Devices"
|
||||
"name": "Amazon Devices"
|
||||
},
|
||||
"amazon_polly": {
|
||||
"integration_type": "hub",
|
||||
|
||||
@@ -199,6 +199,7 @@ class EntityInfo(TypedDict):
|
||||
"""Entity info."""
|
||||
|
||||
domain: str
|
||||
custom_component: bool
|
||||
config_entry: NotRequired[str]
|
||||
|
||||
|
||||
@@ -1449,8 +1450,10 @@ class Entity(
|
||||
|
||||
Not to be extended by integrations.
|
||||
"""
|
||||
is_custom_component = "custom_components" in type(self).__module__
|
||||
entity_info: EntityInfo = {
|
||||
"domain": self.platform.platform_name,
|
||||
"custom_component": is_custom_component,
|
||||
}
|
||||
if self.platform.config_entry:
|
||||
entity_info["config_entry"] = self.platform.config_entry.entry_id
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
aiodhcpwatcher==1.2.0
|
||||
aiodiscover==2.7.0
|
||||
aiodns==3.4.0
|
||||
aiofiles==24.1.0
|
||||
aiohasupervisor==0.3.1
|
||||
aiohttp-asyncmdnsresolver==0.1.1
|
||||
aiohttp-fast-zlib==0.3.0
|
||||
@@ -39,7 +38,7 @@ habluetooth==3.49.0
|
||||
hass-nabucasa==0.101.0
|
||||
hassil==2.2.3
|
||||
home-assistant-bluetooth==1.13.1
|
||||
home-assistant-frontend==20250531.2
|
||||
home-assistant-frontend==20250531.0
|
||||
home-assistant-intents==2025.6.10
|
||||
httpx==0.28.1
|
||||
ifaddr==0.2.0
|
||||
@@ -202,6 +201,14 @@ tenacity!=8.4.0
|
||||
# TypeError: 'Timeout' object does not support the context manager protocol
|
||||
async-timeout==4.0.3
|
||||
|
||||
# aiofiles keeps getting downgraded by custom components
|
||||
# causing newer methods to not be available and breaking
|
||||
# some integrations at startup
|
||||
# https://github.com/home-assistant/core/issues/127529
|
||||
# https://github.com/home-assistant/core/issues/122508
|
||||
# https://github.com/home-assistant/core/issues/118004
|
||||
aiofiles>=24.1.0
|
||||
|
||||
# multidict < 6.4.0 has memory leaks
|
||||
# https://github.com/aio-libs/multidict/issues/1134
|
||||
# https://github.com/aio-libs/multidict/issues/1131
|
||||
|
||||
@@ -405,7 +405,7 @@ disallow_untyped_defs = true
|
||||
warn_return_any = true
|
||||
warn_unreachable = true
|
||||
|
||||
[mypy-homeassistant.components.alexa_devices.*]
|
||||
[mypy-homeassistant.components.alpha_vantage.*]
|
||||
check_untyped_defs = true
|
||||
disallow_incomplete_defs = true
|
||||
disallow_subclassing_any = true
|
||||
@@ -415,7 +415,7 @@ disallow_untyped_defs = true
|
||||
warn_return_any = true
|
||||
warn_unreachable = true
|
||||
|
||||
[mypy-homeassistant.components.alpha_vantage.*]
|
||||
[mypy-homeassistant.components.amazon_devices.*]
|
||||
check_untyped_defs = true
|
||||
disallow_incomplete_defs = true
|
||||
disallow_subclassing_any = true
|
||||
|
||||
@@ -25,6 +25,18 @@ _OBSOLETE_IMPORT: dict[str, list[ObsoleteImportMatch]] = {
|
||||
constant=re.compile(r"^cached_property$"),
|
||||
),
|
||||
],
|
||||
"homeassistant.backports.enum": [
|
||||
ObsoleteImportMatch(
|
||||
reason="We can now use the Python 3.11 provided enum.StrEnum instead",
|
||||
constant=re.compile(r"^StrEnum$"),
|
||||
),
|
||||
],
|
||||
"homeassistant.backports.functools": [
|
||||
ObsoleteImportMatch(
|
||||
reason="replaced by propcache.api.cached_property",
|
||||
constant=re.compile(r"^cached_property$"),
|
||||
),
|
||||
],
|
||||
"homeassistant.components.light": [
|
||||
ObsoleteImportMatch(
|
||||
reason="replaced by ColorMode enum",
|
||||
|
||||
@@ -24,7 +24,6 @@ classifiers = [
|
||||
requires-python = ">=3.13.2"
|
||||
dependencies = [
|
||||
"aiodns==3.4.0",
|
||||
"aiofiles==24.1.0",
|
||||
# Integrations may depend on hassio integration without listing it to
|
||||
# change behavior based on presence of supervisor. Deprecated with #127228
|
||||
# Lib can be removed with 2025.11
|
||||
|
||||
Generated
-1
@@ -4,7 +4,6 @@
|
||||
|
||||
# Home Assistant Core
|
||||
aiodns==3.4.0
|
||||
aiofiles==24.1.0
|
||||
aiohasupervisor==0.3.1
|
||||
aiohttp==3.12.12
|
||||
aiohttp_cors==0.8.1
|
||||
|
||||
Generated
+5
-5
@@ -181,7 +181,7 @@ aioairzone-cloud==0.6.12
|
||||
# homeassistant.components.airzone
|
||||
aioairzone==1.0.0
|
||||
|
||||
# homeassistant.components.alexa_devices
|
||||
# homeassistant.components.amazon_devices
|
||||
aioamazondevices==3.0.6
|
||||
|
||||
# homeassistant.components.ambient_network
|
||||
@@ -1133,7 +1133,7 @@ hass-splunk==0.1.1
|
||||
hassil==2.2.3
|
||||
|
||||
# homeassistant.components.jewish_calendar
|
||||
hdate[astral]==1.1.1
|
||||
hdate[astral]==1.1.0
|
||||
|
||||
# homeassistant.components.heatmiser
|
||||
heatmiserV3==2.0.3
|
||||
@@ -1164,7 +1164,7 @@ hole==0.8.0
|
||||
holidays==0.74
|
||||
|
||||
# homeassistant.components.frontend
|
||||
home-assistant-frontend==20250531.2
|
||||
home-assistant-frontend==20250531.0
|
||||
|
||||
# homeassistant.components.conversation
|
||||
home-assistant-intents==2025.6.10
|
||||
@@ -1185,7 +1185,7 @@ huawei-lte-api==1.11.0
|
||||
huum==0.7.12
|
||||
|
||||
# homeassistant.components.hyperion
|
||||
hyperion-py==0.7.6
|
||||
hyperion-py==0.7.5
|
||||
|
||||
# homeassistant.components.iammeter
|
||||
iammeter==0.2.1
|
||||
@@ -3162,7 +3162,7 @@ youless-api==2.2.0
|
||||
youtubeaio==1.1.5
|
||||
|
||||
# homeassistant.components.media_extractor
|
||||
yt-dlp[default]==2025.06.09
|
||||
yt-dlp[default]==2025.05.22
|
||||
|
||||
# homeassistant.components.zabbix
|
||||
zabbix-utils==2.0.2
|
||||
|
||||
@@ -27,7 +27,7 @@ pytest-github-actions-annotate-failures==0.3.0
|
||||
pytest-socket==0.7.0
|
||||
pytest-sugar==1.0.0
|
||||
pytest-timeout==2.4.0
|
||||
pytest-unordered==0.7.0
|
||||
pytest-unordered==0.6.1
|
||||
pytest-picked==0.5.1
|
||||
pytest-xdist==3.7.0
|
||||
pytest==8.4.0
|
||||
@@ -35,7 +35,7 @@ requests-mock==1.12.1
|
||||
respx==0.22.0
|
||||
syrupy==4.9.1
|
||||
tqdm==4.67.1
|
||||
types-aiofiles==24.1.0.20250606
|
||||
types-aiofiles==24.1.0.20250516
|
||||
types-atomicwrites==1.4.5.1
|
||||
types-croniter==6.0.0.20250411
|
||||
types-caldav==1.3.0.20250516
|
||||
@@ -49,5 +49,5 @@ types-python-dateutil==2.9.0.20250516
|
||||
types-python-slugify==8.0.2.20240310
|
||||
types-pytz==2025.2.0.20250516
|
||||
types-PyYAML==6.0.12.20250516
|
||||
types-requests==2.32.4.20250611
|
||||
types-requests==2.31.0.3
|
||||
types-xmltodict==0.13.0.3
|
||||
|
||||
Generated
+5
-5
@@ -169,7 +169,7 @@ aioairzone-cloud==0.6.12
|
||||
# homeassistant.components.airzone
|
||||
aioairzone==1.0.0
|
||||
|
||||
# homeassistant.components.alexa_devices
|
||||
# homeassistant.components.amazon_devices
|
||||
aioamazondevices==3.0.6
|
||||
|
||||
# homeassistant.components.ambient_network
|
||||
@@ -988,7 +988,7 @@ hass-nabucasa==0.101.0
|
||||
hassil==2.2.3
|
||||
|
||||
# homeassistant.components.jewish_calendar
|
||||
hdate[astral]==1.1.1
|
||||
hdate[astral]==1.1.0
|
||||
|
||||
# homeassistant.components.here_travel_time
|
||||
here-routing==1.0.1
|
||||
@@ -1010,7 +1010,7 @@ hole==0.8.0
|
||||
holidays==0.74
|
||||
|
||||
# homeassistant.components.frontend
|
||||
home-assistant-frontend==20250531.2
|
||||
home-assistant-frontend==20250531.0
|
||||
|
||||
# homeassistant.components.conversation
|
||||
home-assistant-intents==2025.6.10
|
||||
@@ -1028,7 +1028,7 @@ huawei-lte-api==1.11.0
|
||||
huum==0.7.12
|
||||
|
||||
# homeassistant.components.hyperion
|
||||
hyperion-py==0.7.6
|
||||
hyperion-py==0.7.5
|
||||
|
||||
# homeassistant.components.iaqualink
|
||||
iaqualink==0.5.3
|
||||
@@ -2606,7 +2606,7 @@ youless-api==2.2.0
|
||||
youtubeaio==1.1.5
|
||||
|
||||
# homeassistant.components.media_extractor
|
||||
yt-dlp[default]==2025.06.09
|
||||
yt-dlp[default]==2025.05.22
|
||||
|
||||
# homeassistant.components.zamg
|
||||
zamg==0.3.6
|
||||
|
||||
@@ -226,6 +226,14 @@ tenacity!=8.4.0
|
||||
# TypeError: 'Timeout' object does not support the context manager protocol
|
||||
async-timeout==4.0.3
|
||||
|
||||
# aiofiles keeps getting downgraded by custom components
|
||||
# causing newer methods to not be available and breaking
|
||||
# some integrations at startup
|
||||
# https://github.com/home-assistant/core/issues/127529
|
||||
# https://github.com/home-assistant/core/issues/122508
|
||||
# https://github.com/home-assistant/core/issues/118004
|
||||
aiofiles>=24.1.0
|
||||
|
||||
# multidict < 6.4.0 has memory leaks
|
||||
# https://github.com/aio-libs/multidict/issues/1134
|
||||
# https://github.com/aio-libs/multidict/issues/1131
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
"""Tests for the Alexa Devices integration."""
|
||||
"""Tests for the Amazon Devices integration."""
|
||||
|
||||
from homeassistant.core import HomeAssistant
|
||||
|
||||
+6
-6
@@ -1,4 +1,4 @@
|
||||
"""Alexa Devices tests configuration."""
|
||||
"""Amazon Devices tests configuration."""
|
||||
|
||||
from collections.abc import Generator
|
||||
from unittest.mock import AsyncMock, patch
|
||||
@@ -7,7 +7,7 @@ from aioamazondevices.api import AmazonDevice
|
||||
from aioamazondevices.const import DEVICE_TYPE_TO_MODEL
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.alexa_devices.const import CONF_LOGIN_DATA, DOMAIN
|
||||
from homeassistant.components.amazon_devices.const import CONF_LOGIN_DATA, DOMAIN
|
||||
from homeassistant.const import CONF_COUNTRY, CONF_PASSWORD, CONF_USERNAME
|
||||
|
||||
from .const import TEST_COUNTRY, TEST_PASSWORD, TEST_SERIAL_NUMBER, TEST_USERNAME
|
||||
@@ -19,7 +19,7 @@ from tests.common import MockConfigEntry
|
||||
def mock_setup_entry() -> Generator[AsyncMock]:
|
||||
"""Override async_setup_entry."""
|
||||
with patch(
|
||||
"homeassistant.components.alexa_devices.async_setup_entry",
|
||||
"homeassistant.components.amazon_devices.async_setup_entry",
|
||||
return_value=True,
|
||||
) as mock_setup_entry:
|
||||
yield mock_setup_entry
|
||||
@@ -27,14 +27,14 @@ def mock_setup_entry() -> Generator[AsyncMock]:
|
||||
|
||||
@pytest.fixture
|
||||
def mock_amazon_devices_client() -> Generator[AsyncMock]:
|
||||
"""Mock an Alexa Devices client."""
|
||||
"""Mock an Amazon Devices client."""
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.alexa_devices.coordinator.AmazonEchoApi",
|
||||
"homeassistant.components.amazon_devices.coordinator.AmazonEchoApi",
|
||||
autospec=True,
|
||||
) as mock_client,
|
||||
patch(
|
||||
"homeassistant.components.alexa_devices.config_flow.AmazonEchoApi",
|
||||
"homeassistant.components.amazon_devices.config_flow.AmazonEchoApi",
|
||||
new=mock_client,
|
||||
),
|
||||
):
|
||||
@@ -1,4 +1,4 @@
|
||||
"""Alexa Devices tests const."""
|
||||
"""Amazon Devices tests const."""
|
||||
|
||||
TEST_CODE = "023123"
|
||||
TEST_COUNTRY = "IT"
|
||||
+2
-2
@@ -25,7 +25,7 @@
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'Bluetooth',
|
||||
'platform': 'alexa_devices',
|
||||
'platform': 'amazon_devices',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
@@ -73,7 +73,7 @@
|
||||
'original_device_class': <BinarySensorDeviceClass.CONNECTIVITY: 'connectivity'>,
|
||||
'original_icon': None,
|
||||
'original_name': 'Connectivity',
|
||||
'platform': 'alexa_devices',
|
||||
'platform': 'amazon_devices',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
+1
-1
@@ -57,7 +57,7 @@
|
||||
'disabled_by': None,
|
||||
'discovery_keys': dict({
|
||||
}),
|
||||
'domain': 'alexa_devices',
|
||||
'domain': 'amazon_devices',
|
||||
'minor_version': 1,
|
||||
'options': dict({
|
||||
}),
|
||||
+1
-1
@@ -13,7 +13,7 @@
|
||||
'id': <ANY>,
|
||||
'identifiers': set({
|
||||
tuple(
|
||||
'alexa_devices',
|
||||
'amazon_devices',
|
||||
'echo_test_serial_number',
|
||||
),
|
||||
}),
|
||||
+2
-2
@@ -25,7 +25,7 @@
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'Announce',
|
||||
'platform': 'alexa_devices',
|
||||
'platform': 'amazon_devices',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
@@ -74,7 +74,7 @@
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'Speak',
|
||||
'platform': 'alexa_devices',
|
||||
'platform': 'amazon_devices',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
+1
-1
@@ -25,7 +25,7 @@
|
||||
'original_device_class': None,
|
||||
'original_icon': None,
|
||||
'original_name': 'Do not disturb',
|
||||
'platform': 'alexa_devices',
|
||||
'platform': 'amazon_devices',
|
||||
'previous_unique_id': None,
|
||||
'suggested_object_id': None,
|
||||
'supported_features': 0,
|
||||
+3
-3
@@ -1,4 +1,4 @@
|
||||
"""Tests for the Alexa Devices binary sensor platform."""
|
||||
"""Tests for the Amazon Devices binary sensor platform."""
|
||||
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
@@ -11,7 +11,7 @@ from freezegun.api import FrozenDateTimeFactory
|
||||
import pytest
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.components.alexa_devices.coordinator import SCAN_INTERVAL
|
||||
from homeassistant.components.amazon_devices.coordinator import SCAN_INTERVAL
|
||||
from homeassistant.const import STATE_ON, STATE_UNAVAILABLE, Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
@@ -31,7 +31,7 @@ async def test_all_entities(
|
||||
) -> None:
|
||||
"""Test all entities."""
|
||||
with patch(
|
||||
"homeassistant.components.alexa_devices.PLATFORMS", [Platform.BINARY_SENSOR]
|
||||
"homeassistant.components.amazon_devices.PLATFORMS", [Platform.BINARY_SENSOR]
|
||||
):
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
+2
-2
@@ -1,11 +1,11 @@
|
||||
"""Tests for the Alexa Devices config flow."""
|
||||
"""Tests for the Amazon Devices config flow."""
|
||||
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
from aioamazondevices.exceptions import CannotAuthenticate, CannotConnect
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.alexa_devices.const import CONF_LOGIN_DATA, DOMAIN
|
||||
from homeassistant.components.amazon_devices.const import CONF_LOGIN_DATA, DOMAIN
|
||||
from homeassistant.config_entries import SOURCE_USER
|
||||
from homeassistant.const import CONF_CODE, CONF_COUNTRY, CONF_PASSWORD, CONF_USERNAME
|
||||
from homeassistant.core import HomeAssistant
|
||||
+2
-2
@@ -1,4 +1,4 @@
|
||||
"""Tests for Alexa Devices diagnostics platform."""
|
||||
"""Tests for Amazon Devices diagnostics platform."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -7,7 +7,7 @@ from unittest.mock import AsyncMock
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
from syrupy.filters import props
|
||||
|
||||
from homeassistant.components.alexa_devices.const import DOMAIN
|
||||
from homeassistant.components.amazon_devices.const import DOMAIN
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
|
||||
+2
-2
@@ -1,10 +1,10 @@
|
||||
"""Tests for the Alexa Devices integration."""
|
||||
"""Tests for the Amazon Devices integration."""
|
||||
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.components.alexa_devices.const import DOMAIN
|
||||
from homeassistant.components.amazon_devices.const import DOMAIN
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import device_registry as dr
|
||||
|
||||
+3
-3
@@ -1,4 +1,4 @@
|
||||
"""Tests for the Alexa Devices notify platform."""
|
||||
"""Tests for the Amazon Devices notify platform."""
|
||||
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
@@ -6,7 +6,7 @@ from freezegun.api import FrozenDateTimeFactory
|
||||
import pytest
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.components.alexa_devices.coordinator import SCAN_INTERVAL
|
||||
from homeassistant.components.amazon_devices.coordinator import SCAN_INTERVAL
|
||||
from homeassistant.components.notify import (
|
||||
ATTR_MESSAGE,
|
||||
DOMAIN as NOTIFY_DOMAIN,
|
||||
@@ -32,7 +32,7 @@ async def test_all_entities(
|
||||
entity_registry: er.EntityRegistry,
|
||||
) -> None:
|
||||
"""Test all entities."""
|
||||
with patch("homeassistant.components.alexa_devices.PLATFORMS", [Platform.NOTIFY]):
|
||||
with patch("homeassistant.components.amazon_devices.PLATFORMS", [Platform.NOTIFY]):
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
|
||||
+3
-3
@@ -1,4 +1,4 @@
|
||||
"""Tests for the Alexa Devices switch platform."""
|
||||
"""Tests for the Amazon Devices switch platform."""
|
||||
|
||||
from unittest.mock import AsyncMock, patch
|
||||
|
||||
@@ -6,7 +6,7 @@ from freezegun.api import FrozenDateTimeFactory
|
||||
import pytest
|
||||
from syrupy.assertion import SnapshotAssertion
|
||||
|
||||
from homeassistant.components.alexa_devices.coordinator import SCAN_INTERVAL
|
||||
from homeassistant.components.amazon_devices.coordinator import SCAN_INTERVAL
|
||||
from homeassistant.components.switch import (
|
||||
DOMAIN as SWITCH_DOMAIN,
|
||||
SERVICE_TURN_OFF,
|
||||
@@ -37,7 +37,7 @@ async def test_all_entities(
|
||||
entity_registry: er.EntityRegistry,
|
||||
) -> None:
|
||||
"""Test all entities."""
|
||||
with patch("homeassistant.components.alexa_devices.PLATFORMS", [Platform.SWITCH]):
|
||||
with patch("homeassistant.components.amazon_devices.PLATFORMS", [Platform.SWITCH]):
|
||||
await setup_integration(hass, mock_config_entry)
|
||||
|
||||
await snapshot_platform(hass, entity_registry, snapshot, mock_config_entry.entry_id)
|
||||
@@ -126,8 +126,7 @@ async def test_command_line_output_single_command(
|
||||
await hass.services.async_call(
|
||||
NOTIFY_DOMAIN, "test3", {"message": "test message"}, blocking=True
|
||||
)
|
||||
assert "Running command: echo" in caplog.text
|
||||
assert "Running with message: test message" in caplog.text
|
||||
assert "Running command: echo, with message: test message" in caplog.text
|
||||
|
||||
|
||||
async def test_command_template(hass: HomeAssistant) -> None:
|
||||
|
||||
@@ -83,8 +83,8 @@ def mock_power_sensor() -> Mock:
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_positionable_cover() -> Mock:
|
||||
"""Fixture for a positionable cover."""
|
||||
def mock_cover() -> Mock:
|
||||
"""Fixture for a cover."""
|
||||
cover = Mock()
|
||||
cover.fibaro_id = 3
|
||||
cover.parent_fibaro_id = 0
|
||||
@@ -112,42 +112,6 @@ def mock_positionable_cover() -> Mock:
|
||||
return cover
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_cover() -> Mock:
|
||||
"""Fixture for a cover supporting slats but without positioning."""
|
||||
cover = Mock()
|
||||
cover.fibaro_id = 4
|
||||
cover.parent_fibaro_id = 0
|
||||
cover.name = "Test cover"
|
||||
cover.room_id = 1
|
||||
cover.dead = False
|
||||
cover.visible = True
|
||||
cover.enabled = True
|
||||
cover.type = "com.fibaro.baseShutter"
|
||||
cover.base_type = "com.fibaro.actor"
|
||||
cover.properties = {"manufacturer": ""}
|
||||
cover.actions = {
|
||||
"open": 0,
|
||||
"close": 0,
|
||||
"stop": 0,
|
||||
"rotateSlatsUp": 0,
|
||||
"rotateSlatsDown": 0,
|
||||
"stopSlats": 0,
|
||||
}
|
||||
cover.supported_features = {}
|
||||
value_mock = Mock()
|
||||
value_mock.has_value = False
|
||||
cover.value = value_mock
|
||||
value2_mock = Mock()
|
||||
value2_mock.has_value = False
|
||||
cover.value_2 = value2_mock
|
||||
state_mock = Mock()
|
||||
state_mock.has_value = True
|
||||
state_mock.str_value.return_value = "closed"
|
||||
cover.state = state_mock
|
||||
return cover
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_light() -> Mock:
|
||||
"""Fixture for a dimmmable light."""
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
from homeassistant.components.cover import CoverEntityFeature, CoverState
|
||||
from homeassistant.components.cover import CoverState
|
||||
from homeassistant.const import Platform
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import entity_registry as er
|
||||
@@ -12,98 +12,6 @@ from .conftest import init_integration
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
async def test_positionable_cover_setup(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
mock_fibaro_client: Mock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_positionable_cover: Mock,
|
||||
mock_room: Mock,
|
||||
) -> None:
|
||||
"""Test that the cover creates an entity."""
|
||||
|
||||
# Arrange
|
||||
mock_fibaro_client.read_rooms.return_value = [mock_room]
|
||||
mock_fibaro_client.read_devices.return_value = [mock_positionable_cover]
|
||||
|
||||
with patch("homeassistant.components.fibaro.PLATFORMS", [Platform.COVER]):
|
||||
# Act
|
||||
await init_integration(hass, mock_config_entry)
|
||||
# Assert
|
||||
entry = entity_registry.async_get("cover.room_1_test_cover_3")
|
||||
assert entry
|
||||
assert entry.supported_features == (
|
||||
CoverEntityFeature.OPEN
|
||||
| CoverEntityFeature.CLOSE
|
||||
| CoverEntityFeature.STOP
|
||||
| CoverEntityFeature.SET_POSITION
|
||||
)
|
||||
assert entry.unique_id == "hc2_111111.3"
|
||||
assert entry.original_name == "Room 1 Test cover"
|
||||
|
||||
|
||||
async def test_cover_opening(
|
||||
hass: HomeAssistant,
|
||||
mock_fibaro_client: Mock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_positionable_cover: Mock,
|
||||
mock_room: Mock,
|
||||
) -> None:
|
||||
"""Test that the cover opening state is reported."""
|
||||
|
||||
# Arrange
|
||||
mock_fibaro_client.read_rooms.return_value = [mock_room]
|
||||
mock_fibaro_client.read_devices.return_value = [mock_positionable_cover]
|
||||
|
||||
with patch("homeassistant.components.fibaro.PLATFORMS", [Platform.COVER]):
|
||||
# Act
|
||||
await init_integration(hass, mock_config_entry)
|
||||
# Assert
|
||||
assert hass.states.get("cover.room_1_test_cover_3").state == CoverState.OPENING
|
||||
|
||||
|
||||
async def test_cover_opening_closing_none(
|
||||
hass: HomeAssistant,
|
||||
mock_fibaro_client: Mock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_positionable_cover: Mock,
|
||||
mock_room: Mock,
|
||||
) -> None:
|
||||
"""Test that the cover opening closing states return None if not available."""
|
||||
|
||||
# Arrange
|
||||
mock_fibaro_client.read_rooms.return_value = [mock_room]
|
||||
mock_positionable_cover.state.str_value.return_value = ""
|
||||
mock_fibaro_client.read_devices.return_value = [mock_positionable_cover]
|
||||
|
||||
with patch("homeassistant.components.fibaro.PLATFORMS", [Platform.COVER]):
|
||||
# Act
|
||||
await init_integration(hass, mock_config_entry)
|
||||
# Assert
|
||||
assert hass.states.get("cover.room_1_test_cover_3").state == CoverState.OPEN
|
||||
|
||||
|
||||
async def test_cover_closing(
|
||||
hass: HomeAssistant,
|
||||
mock_fibaro_client: Mock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_positionable_cover: Mock,
|
||||
mock_room: Mock,
|
||||
) -> None:
|
||||
"""Test that the cover closing state is reported."""
|
||||
|
||||
# Arrange
|
||||
mock_fibaro_client.read_rooms.return_value = [mock_room]
|
||||
mock_positionable_cover.state.str_value.return_value = "closing"
|
||||
mock_fibaro_client.read_devices.return_value = [mock_positionable_cover]
|
||||
|
||||
with patch("homeassistant.components.fibaro.PLATFORMS", [Platform.COVER]):
|
||||
# Act
|
||||
await init_integration(hass, mock_config_entry)
|
||||
# Assert
|
||||
assert hass.states.get("cover.room_1_test_cover_3").state == CoverState.CLOSING
|
||||
|
||||
|
||||
async def test_cover_setup(
|
||||
hass: HomeAssistant,
|
||||
entity_registry: er.EntityRegistry,
|
||||
@@ -122,28 +30,20 @@ async def test_cover_setup(
|
||||
# Act
|
||||
await init_integration(hass, mock_config_entry)
|
||||
# Assert
|
||||
entry = entity_registry.async_get("cover.room_1_test_cover_4")
|
||||
entry = entity_registry.async_get("cover.room_1_test_cover_3")
|
||||
assert entry
|
||||
assert entry.supported_features == (
|
||||
CoverEntityFeature.OPEN
|
||||
| CoverEntityFeature.CLOSE
|
||||
| CoverEntityFeature.STOP
|
||||
| CoverEntityFeature.OPEN_TILT
|
||||
| CoverEntityFeature.CLOSE_TILT
|
||||
| CoverEntityFeature.STOP_TILT
|
||||
)
|
||||
assert entry.unique_id == "hc2_111111.4"
|
||||
assert entry.unique_id == "hc2_111111.3"
|
||||
assert entry.original_name == "Room 1 Test cover"
|
||||
|
||||
|
||||
async def test_cover_open_action(
|
||||
async def test_cover_opening(
|
||||
hass: HomeAssistant,
|
||||
mock_fibaro_client: Mock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_cover: Mock,
|
||||
mock_room: Mock,
|
||||
) -> None:
|
||||
"""Test that open_cover works."""
|
||||
"""Test that the cover opening state is reported."""
|
||||
|
||||
# Arrange
|
||||
mock_fibaro_client.read_rooms.return_value = [mock_room]
|
||||
@@ -152,147 +52,47 @@ async def test_cover_open_action(
|
||||
with patch("homeassistant.components.fibaro.PLATFORMS", [Platform.COVER]):
|
||||
# Act
|
||||
await init_integration(hass, mock_config_entry)
|
||||
await hass.services.async_call(
|
||||
"cover",
|
||||
"open_cover",
|
||||
{"entity_id": "cover.room_1_test_cover_4"},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
# Assert
|
||||
mock_cover.execute_action.assert_called_once_with("open", ())
|
||||
assert hass.states.get("cover.room_1_test_cover_3").state == CoverState.OPENING
|
||||
|
||||
|
||||
async def test_cover_close_action(
|
||||
async def test_cover_opening_closing_none(
|
||||
hass: HomeAssistant,
|
||||
mock_fibaro_client: Mock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_cover: Mock,
|
||||
mock_room: Mock,
|
||||
) -> None:
|
||||
"""Test that close_cover works."""
|
||||
"""Test that the cover opening closing states return None if not available."""
|
||||
|
||||
# Arrange
|
||||
mock_fibaro_client.read_rooms.return_value = [mock_room]
|
||||
mock_cover.state.has_value = False
|
||||
mock_fibaro_client.read_devices.return_value = [mock_cover]
|
||||
|
||||
with patch("homeassistant.components.fibaro.PLATFORMS", [Platform.COVER]):
|
||||
# Act
|
||||
await init_integration(hass, mock_config_entry)
|
||||
await hass.services.async_call(
|
||||
"cover",
|
||||
"close_cover",
|
||||
{"entity_id": "cover.room_1_test_cover_4"},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
# Assert
|
||||
mock_cover.execute_action.assert_called_once_with("close", ())
|
||||
assert hass.states.get("cover.room_1_test_cover_3").state == CoverState.OPEN
|
||||
|
||||
|
||||
async def test_cover_stop_action(
|
||||
async def test_cover_closing(
|
||||
hass: HomeAssistant,
|
||||
mock_fibaro_client: Mock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_cover: Mock,
|
||||
mock_room: Mock,
|
||||
) -> None:
|
||||
"""Test that stop_cover works."""
|
||||
"""Test that the cover closing state is reported."""
|
||||
|
||||
# Arrange
|
||||
mock_fibaro_client.read_rooms.return_value = [mock_room]
|
||||
mock_cover.state.str_value.return_value = "closing"
|
||||
mock_fibaro_client.read_devices.return_value = [mock_cover]
|
||||
|
||||
with patch("homeassistant.components.fibaro.PLATFORMS", [Platform.COVER]):
|
||||
# Act
|
||||
await init_integration(hass, mock_config_entry)
|
||||
await hass.services.async_call(
|
||||
"cover",
|
||||
"stop_cover",
|
||||
{"entity_id": "cover.room_1_test_cover_4"},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
# Assert
|
||||
mock_cover.execute_action.assert_called_once_with("stop", ())
|
||||
|
||||
|
||||
async def test_cover_open_slats_action(
|
||||
hass: HomeAssistant,
|
||||
mock_fibaro_client: Mock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_cover: Mock,
|
||||
mock_room: Mock,
|
||||
) -> None:
|
||||
"""Test that open_cover_tilt works."""
|
||||
|
||||
# Arrange
|
||||
mock_fibaro_client.read_rooms.return_value = [mock_room]
|
||||
mock_fibaro_client.read_devices.return_value = [mock_cover]
|
||||
|
||||
with patch("homeassistant.components.fibaro.PLATFORMS", [Platform.COVER]):
|
||||
# Act
|
||||
await init_integration(hass, mock_config_entry)
|
||||
await hass.services.async_call(
|
||||
"cover",
|
||||
"open_cover_tilt",
|
||||
{"entity_id": "cover.room_1_test_cover_4"},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
# Assert
|
||||
mock_cover.execute_action.assert_called_once_with("rotateSlatsUp", ())
|
||||
|
||||
|
||||
async def test_cover_close_tilt_action(
|
||||
hass: HomeAssistant,
|
||||
mock_fibaro_client: Mock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_cover: Mock,
|
||||
mock_room: Mock,
|
||||
) -> None:
|
||||
"""Test that close_cover_tilt works."""
|
||||
|
||||
# Arrange
|
||||
mock_fibaro_client.read_rooms.return_value = [mock_room]
|
||||
mock_fibaro_client.read_devices.return_value = [mock_cover]
|
||||
|
||||
with patch("homeassistant.components.fibaro.PLATFORMS", [Platform.COVER]):
|
||||
# Act
|
||||
await init_integration(hass, mock_config_entry)
|
||||
await hass.services.async_call(
|
||||
"cover",
|
||||
"close_cover_tilt",
|
||||
{"entity_id": "cover.room_1_test_cover_4"},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
# Assert
|
||||
mock_cover.execute_action.assert_called_once_with("rotateSlatsDown", ())
|
||||
|
||||
|
||||
async def test_cover_stop_slats_action(
|
||||
hass: HomeAssistant,
|
||||
mock_fibaro_client: Mock,
|
||||
mock_config_entry: MockConfigEntry,
|
||||
mock_cover: Mock,
|
||||
mock_room: Mock,
|
||||
) -> None:
|
||||
"""Test that stop_cover_tilt works."""
|
||||
|
||||
# Arrange
|
||||
mock_fibaro_client.read_rooms.return_value = [mock_room]
|
||||
mock_fibaro_client.read_devices.return_value = [mock_cover]
|
||||
|
||||
with patch("homeassistant.components.fibaro.PLATFORMS", [Platform.COVER]):
|
||||
# Act
|
||||
await init_integration(hass, mock_config_entry)
|
||||
await hass.services.async_call(
|
||||
"cover",
|
||||
"stop_cover_tilt",
|
||||
{"entity_id": "cover.room_1_test_cover_4"},
|
||||
blocking=True,
|
||||
)
|
||||
|
||||
# Assert
|
||||
mock_cover.execute_action.assert_called_once_with("stopSlats", ())
|
||||
assert hass.states.get("cover.room_1_test_cover_3").state == CoverState.CLOSING
|
||||
|
||||
@@ -260,16 +260,3 @@ def all_setup_requests(
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def arch() -> str:
|
||||
"""Arch found in apk file."""
|
||||
return "amd64"
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def mock_arch_file(arch: str) -> Generator[None]:
|
||||
"""Mock arch file."""
|
||||
with patch("homeassistant.components.hassio._get_arch", return_value=arch):
|
||||
yield
|
||||
|
||||
@@ -1156,11 +1156,7 @@ def test_deprecated_constants(
|
||||
("rpi2", "deprecated_os_armv7"),
|
||||
],
|
||||
)
|
||||
@pytest.mark.parametrize(
|
||||
"arch",
|
||||
["armv7"],
|
||||
)
|
||||
async def test_deprecated_installation_issue_os_armv7(
|
||||
async def test_deprecated_installation_issue_aarch64(
|
||||
hass: HomeAssistant,
|
||||
issue_registry: ir.IssueRegistry,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
@@ -1171,15 +1167,18 @@ async def test_deprecated_installation_issue_os_armv7(
|
||||
with (
|
||||
patch.dict(os.environ, MOCK_ENVIRON),
|
||||
patch(
|
||||
"homeassistant.components.homeassistant.async_get_system_info",
|
||||
"homeassistant.components.hassio.async_get_system_info",
|
||||
return_value={
|
||||
"installation_type": "Home Assistant OS",
|
||||
"arch": "armv7",
|
||||
},
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.hassio._is_32_bit",
|
||||
return_value=True,
|
||||
"homeassistant.components.homeassistant.async_get_system_info",
|
||||
return_value={
|
||||
"installation_type": "Home Assistant OS",
|
||||
"arch": "armv7",
|
||||
},
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.hassio.get_os_info", return_value={"board": board}
|
||||
@@ -1229,7 +1228,7 @@ async def test_deprecated_installation_issue_os_armv7(
|
||||
"armv7",
|
||||
],
|
||||
)
|
||||
async def test_deprecated_installation_issue_32bit_os(
|
||||
async def test_deprecated_installation_issue_32bit_method(
|
||||
hass: HomeAssistant,
|
||||
issue_registry: ir.IssueRegistry,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
@@ -1239,15 +1238,18 @@ async def test_deprecated_installation_issue_32bit_os(
|
||||
with (
|
||||
patch.dict(os.environ, MOCK_ENVIRON),
|
||||
patch(
|
||||
"homeassistant.components.homeassistant.async_get_system_info",
|
||||
"homeassistant.components.hassio.async_get_system_info",
|
||||
return_value={
|
||||
"installation_type": "Home Assistant OS",
|
||||
"arch": arch,
|
||||
},
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.hassio._is_32_bit",
|
||||
return_value=True,
|
||||
"homeassistant.components.homeassistant.async_get_system_info",
|
||||
return_value={
|
||||
"installation_type": "Home Assistant OS",
|
||||
"arch": arch,
|
||||
},
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.hassio.get_os_info",
|
||||
@@ -1306,15 +1308,18 @@ async def test_deprecated_installation_issue_32bit_supervised(
|
||||
with (
|
||||
patch.dict(os.environ, MOCK_ENVIRON),
|
||||
patch(
|
||||
"homeassistant.components.homeassistant.async_get_system_info",
|
||||
"homeassistant.components.hassio.async_get_system_info",
|
||||
return_value={
|
||||
"installation_type": "Home Assistant Supervised",
|
||||
"arch": arch,
|
||||
},
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.hassio._is_32_bit",
|
||||
return_value=True,
|
||||
"homeassistant.components.homeassistant.async_get_system_info",
|
||||
return_value={
|
||||
"installation_type": "Home Assistant Supervised",
|
||||
"arch": arch,
|
||||
},
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.hassio.get_os_info",
|
||||
@@ -1360,75 +1365,6 @@ async def test_deprecated_installation_issue_32bit_supervised(
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"arch",
|
||||
[
|
||||
"amd64",
|
||||
"aarch64",
|
||||
],
|
||||
)
|
||||
async def test_deprecated_installation_issue_64bit_supervised(
|
||||
hass: HomeAssistant,
|
||||
issue_registry: ir.IssueRegistry,
|
||||
freezer: FrozenDateTimeFactory,
|
||||
arch: str,
|
||||
) -> None:
|
||||
"""Test deprecated architecture issue."""
|
||||
with (
|
||||
patch.dict(os.environ, MOCK_ENVIRON),
|
||||
patch(
|
||||
"homeassistant.components.homeassistant.async_get_system_info",
|
||||
return_value={
|
||||
"installation_type": "Home Assistant Supervised",
|
||||
"arch": arch,
|
||||
},
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.hassio._is_32_bit",
|
||||
return_value=False,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.hassio.get_os_info",
|
||||
return_value={"board": "generic-x86-64"},
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.hassio.get_info", return_value={"hassos": None}
|
||||
),
|
||||
patch("homeassistant.components.hardware.async_setup", return_value=True),
|
||||
):
|
||||
assert await async_setup_component(hass, "homeassistant", {})
|
||||
config_entry = MockConfigEntry(domain=DOMAIN, data={}, unique_id=DOMAIN)
|
||||
config_entry.add_to_hass(hass)
|
||||
assert await hass.config_entries.async_setup(config_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
freezer.tick(REQUEST_REFRESH_DELAY)
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
await hass.services.async_call(
|
||||
"homeassistant",
|
||||
"update_entity",
|
||||
{
|
||||
"entity_id": [
|
||||
"update.home_assistant_core_update",
|
||||
"update.home_assistant_supervisor_update",
|
||||
]
|
||||
},
|
||||
blocking=True,
|
||||
)
|
||||
freezer.tick(HASSIO_UPDATE_INTERVAL)
|
||||
async_fire_time_changed(hass)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert len(issue_registry.issues) == 1
|
||||
issue = issue_registry.async_get_issue("homeassistant", "deprecated_method")
|
||||
assert issue.domain == "homeassistant"
|
||||
assert issue.severity == ir.IssueSeverity.WARNING
|
||||
assert issue.translation_placeholders == {
|
||||
"installation_type": "Supervised",
|
||||
"arch": arch,
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("board", "issue_id"),
|
||||
[
|
||||
@@ -1446,15 +1382,18 @@ async def test_deprecated_installation_issue_supported_board(
|
||||
with (
|
||||
patch.dict(os.environ, MOCK_ENVIRON),
|
||||
patch(
|
||||
"homeassistant.components.homeassistant.async_get_system_info",
|
||||
"homeassistant.components.hassio.async_get_system_info",
|
||||
return_value={
|
||||
"installation_type": "Home Assistant OS",
|
||||
"arch": "aarch64",
|
||||
},
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.hassio._is_32_bit",
|
||||
return_value=False,
|
||||
"homeassistant.components.homeassistant.async_get_system_info",
|
||||
return_value={
|
||||
"installation_type": "Home Assistant OS",
|
||||
"arch": "aarch64",
|
||||
},
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.hassio.get_os_info", return_value={"board": board}
|
||||
|
||||
@@ -648,24 +648,18 @@ async def test_reload_all(
|
||||
"armv7",
|
||||
],
|
||||
)
|
||||
async def test_deprecated_installation_issue_32bit_core(
|
||||
async def test_deprecated_installation_issue_32bit_method(
|
||||
hass: HomeAssistant,
|
||||
issue_registry: ir.IssueRegistry,
|
||||
arch: str,
|
||||
) -> None:
|
||||
"""Test deprecated installation issue."""
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.homeassistant.async_get_system_info",
|
||||
return_value={
|
||||
"installation_type": "Home Assistant Core",
|
||||
"arch": arch,
|
||||
},
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.homeassistant._is_32_bit",
|
||||
return_value=True,
|
||||
),
|
||||
with patch(
|
||||
"homeassistant.components.homeassistant.async_get_system_info",
|
||||
return_value={
|
||||
"installation_type": "Home Assistant Core",
|
||||
"arch": arch,
|
||||
},
|
||||
):
|
||||
assert await async_setup_component(hass, DOMAIN, {})
|
||||
await hass.async_block_till_done()
|
||||
@@ -683,28 +677,46 @@ async def test_deprecated_installation_issue_32bit_core(
|
||||
@pytest.mark.parametrize(
|
||||
"arch",
|
||||
[
|
||||
"aarch64",
|
||||
"generic-x86-64",
|
||||
"i386",
|
||||
"armhf",
|
||||
],
|
||||
)
|
||||
async def test_deprecated_installation_issue_64bit_core(
|
||||
async def test_deprecated_installation_issue_32bit(
|
||||
hass: HomeAssistant,
|
||||
issue_registry: ir.IssueRegistry,
|
||||
arch: str,
|
||||
) -> None:
|
||||
"""Test deprecated installation issue."""
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.homeassistant.async_get_system_info",
|
||||
return_value={
|
||||
"installation_type": "Home Assistant Core",
|
||||
"arch": arch,
|
||||
},
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.homeassistant._is_32_bit",
|
||||
return_value=False,
|
||||
),
|
||||
with patch(
|
||||
"homeassistant.components.homeassistant.async_get_system_info",
|
||||
return_value={
|
||||
"installation_type": "Home Assistant Container",
|
||||
"arch": arch,
|
||||
},
|
||||
):
|
||||
assert await async_setup_component(hass, DOMAIN, {})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert len(issue_registry.issues) == 1
|
||||
issue = issue_registry.async_get_issue(DOMAIN, "deprecated_architecture")
|
||||
assert issue.domain == DOMAIN
|
||||
assert issue.severity == ir.IssueSeverity.WARNING
|
||||
assert issue.translation_placeholders == {
|
||||
"installation_type": "Container",
|
||||
"arch": arch,
|
||||
}
|
||||
|
||||
|
||||
async def test_deprecated_installation_issue_method(
|
||||
hass: HomeAssistant, issue_registry: ir.IssueRegistry
|
||||
) -> None:
|
||||
"""Test deprecated installation issue."""
|
||||
with patch(
|
||||
"homeassistant.components.homeassistant.async_get_system_info",
|
||||
return_value={
|
||||
"installation_type": "Home Assistant Core",
|
||||
"arch": "generic-x86-64",
|
||||
},
|
||||
):
|
||||
assert await async_setup_component(hass, DOMAIN, {})
|
||||
await hass.async_block_till_done()
|
||||
@@ -715,46 +727,26 @@ async def test_deprecated_installation_issue_64bit_core(
|
||||
assert issue.severity == ir.IssueSeverity.WARNING
|
||||
assert issue.translation_placeholders == {
|
||||
"installation_type": "Core",
|
||||
"arch": arch,
|
||||
"arch": "generic-x86-64",
|
||||
}
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"arch",
|
||||
[
|
||||
"i386",
|
||||
"armv7",
|
||||
"armhf",
|
||||
],
|
||||
)
|
||||
async def test_deprecated_installation_issue_32bit(
|
||||
async def test_deprecated_installation_issue_armv7_container(
|
||||
hass: HomeAssistant,
|
||||
issue_registry: ir.IssueRegistry,
|
||||
arch: str,
|
||||
) -> None:
|
||||
"""Test deprecated installation issue."""
|
||||
with (
|
||||
patch(
|
||||
"homeassistant.components.homeassistant.async_get_system_info",
|
||||
return_value={
|
||||
"installation_type": "Home Assistant Container",
|
||||
"arch": arch,
|
||||
},
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.homeassistant._is_32_bit",
|
||||
return_value=True,
|
||||
),
|
||||
patch(
|
||||
"homeassistant.components.homeassistant._get_arch",
|
||||
return_value=arch,
|
||||
),
|
||||
with patch(
|
||||
"homeassistant.components.homeassistant.async_get_system_info",
|
||||
return_value={
|
||||
"installation_type": "Home Assistant Container",
|
||||
"arch": "armv7",
|
||||
},
|
||||
):
|
||||
assert await async_setup_component(hass, DOMAIN, {})
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert len(issue_registry.issues) == 1
|
||||
issue = issue_registry.async_get_issue(DOMAIN, "deprecated_container")
|
||||
issue = issue_registry.async_get_issue(DOMAIN, "deprecated_container_armv7")
|
||||
assert issue.domain == DOMAIN
|
||||
assert issue.severity == ir.IssueSeverity.WARNING
|
||||
assert issue.translation_placeholders == {"arch": arch}
|
||||
|
||||
@@ -25,6 +25,14 @@ MOCK_CONFIG_ENTRY_DATA = {
|
||||
CONF_VERIFY_SSL: False,
|
||||
}
|
||||
|
||||
MOCK_CONFIG_ENTRY_DATA_HASSIO = {
|
||||
CONF_HOST: "172.16.10.1",
|
||||
CONF_API_KEY: "abcdef0123456789",
|
||||
CONF_PORT: 8080,
|
||||
CONF_SSL: False,
|
||||
CONF_VERIFY_SSL: False,
|
||||
}
|
||||
|
||||
ALBUM_DATA = {
|
||||
"id": "721e1a4b-aa12-441e-8d3b-5ac7ab283bb6",
|
||||
"albumName": "My Album",
|
||||
|
||||
@@ -7,12 +7,13 @@ from aioimmich.exceptions import ImmichUnauthorizedError
|
||||
import pytest
|
||||
|
||||
from homeassistant.components.immich.const import DOMAIN
|
||||
from homeassistant.config_entries import SOURCE_USER
|
||||
from homeassistant.const import CONF_API_KEY, CONF_URL
|
||||
from homeassistant.config_entries import SOURCE_HASSIO, SOURCE_USER
|
||||
from homeassistant.const import CONF_API_KEY, CONF_URL, CONF_VERIFY_SSL
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.data_entry_flow import FlowResultType
|
||||
from homeassistant.helpers.service_info.hassio import HassioServiceInfo
|
||||
|
||||
from .const import MOCK_CONFIG_ENTRY_DATA, MOCK_USER_DATA
|
||||
from .const import MOCK_CONFIG_ENTRY_DATA, MOCK_CONFIG_ENTRY_DATA_HASSIO, MOCK_USER_DATA
|
||||
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
@@ -242,3 +243,121 @@ async def test_reauth_flow_mismatch(
|
||||
|
||||
assert result["type"] is FlowResultType.ABORT
|
||||
assert result["reason"] == "unique_id_mismatch"
|
||||
|
||||
|
||||
async def test_hassio_flow(
|
||||
hass: HomeAssistant, mock_setup_entry: AsyncMock, mock_immich: Mock
|
||||
) -> None:
|
||||
"""Test discovery via hassio."""
|
||||
|
||||
test_data = HassioServiceInfo(
|
||||
config={
|
||||
"host": "172.16.10.1",
|
||||
"port": 8080,
|
||||
"ssl": False,
|
||||
"addon": "IMMICH",
|
||||
},
|
||||
name="IMMICH",
|
||||
slug="immich",
|
||||
uuid="1234",
|
||||
)
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_HASSIO},
|
||||
data=test_data,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "hassio_confirm"
|
||||
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
CONF_API_KEY: MOCK_USER_DATA[CONF_API_KEY],
|
||||
CONF_VERIFY_SSL: MOCK_USER_DATA[CONF_VERIFY_SSL],
|
||||
},
|
||||
)
|
||||
|
||||
assert result2["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result2["title"] == "user"
|
||||
assert result2["data"] == MOCK_CONFIG_ENTRY_DATA_HASSIO
|
||||
assert result2["result"].unique_id == "e7ef5713-9dab-4bd4-b899-715b0ca4379e"
|
||||
assert len(mock_setup_entry.mock_calls) == 1
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("exception", "error"),
|
||||
[
|
||||
(
|
||||
ImmichUnauthorizedError(
|
||||
{
|
||||
"message": "Invalid API key",
|
||||
"error": "Unauthenticated",
|
||||
"statusCode": 401,
|
||||
"correlationId": "abcdefg",
|
||||
}
|
||||
),
|
||||
"invalid_auth",
|
||||
),
|
||||
(ClientError, "cannot_connect"),
|
||||
(Exception, "unknown"),
|
||||
],
|
||||
)
|
||||
async def test_hassio_flow_error_handling(
|
||||
hass: HomeAssistant,
|
||||
mock_setup_entry: AsyncMock,
|
||||
mock_immich: Mock,
|
||||
exception: Exception,
|
||||
error: str,
|
||||
) -> None:
|
||||
"""Test error handling during discovery via hassio."""
|
||||
|
||||
test_data = HassioServiceInfo(
|
||||
config={
|
||||
"host": "172.16.10.1",
|
||||
"port": 8080,
|
||||
"ssl": False,
|
||||
"addon": "IMMICH",
|
||||
},
|
||||
name="IMMICH",
|
||||
slug="immich",
|
||||
uuid="1234",
|
||||
)
|
||||
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={"source": SOURCE_HASSIO},
|
||||
data=test_data,
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
assert result["type"] is FlowResultType.FORM
|
||||
assert result["step_id"] == "hassio_confirm"
|
||||
|
||||
mock_immich.users.async_get_my_user.side_effect = exception
|
||||
|
||||
result2 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
CONF_API_KEY: MOCK_USER_DATA[CONF_API_KEY],
|
||||
CONF_VERIFY_SSL: MOCK_USER_DATA[CONF_VERIFY_SSL],
|
||||
},
|
||||
)
|
||||
assert result2["type"] is FlowResultType.FORM
|
||||
assert result2["step_id"] == "hassio_confirm"
|
||||
assert result2["errors"] == {"base": error}
|
||||
|
||||
mock_immich.users.async_get_my_user.side_effect = None
|
||||
|
||||
result3 = await hass.config_entries.flow.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
CONF_API_KEY: MOCK_USER_DATA[CONF_API_KEY],
|
||||
CONF_VERIFY_SSL: MOCK_USER_DATA[CONF_VERIFY_SSL],
|
||||
},
|
||||
)
|
||||
|
||||
assert result3["type"] is FlowResultType.CREATE_ENTRY
|
||||
assert result3["title"] == "user"
|
||||
assert result3["data"] == MOCK_CONFIG_ENTRY_DATA_HASSIO
|
||||
assert result3["result"].unique_id == "e7ef5713-9dab-4bd4-b899-715b0ca4379e"
|
||||
|
||||
@@ -260,33 +260,6 @@ MOCK_BLU_TRV_REMOTE_CONFIG = {
|
||||
"meta": {},
|
||||
},
|
||||
},
|
||||
{
|
||||
"key": "blutrv:201",
|
||||
"status": {
|
||||
"id": 201,
|
||||
"target_C": 17.1,
|
||||
"current_C": 17.1,
|
||||
"pos": 0,
|
||||
"rssi": -60,
|
||||
"battery": 100,
|
||||
"packet_id": 58,
|
||||
"last_updated_ts": 1734967725,
|
||||
"paired": True,
|
||||
"rpc": True,
|
||||
"rsv": 61,
|
||||
},
|
||||
"config": {
|
||||
"id": 201,
|
||||
"addr": "f8:44:77:25:f0:de",
|
||||
"name": "TRV-201",
|
||||
"key": None,
|
||||
"trv": "bthomedevice:201",
|
||||
"temp_sensors": [],
|
||||
"dw_sensors": [],
|
||||
"override_delay": 30,
|
||||
"meta": {},
|
||||
},
|
||||
},
|
||||
],
|
||||
"blutrv:200": {
|
||||
"id": 0,
|
||||
@@ -299,17 +272,6 @@ MOCK_BLU_TRV_REMOTE_CONFIG = {
|
||||
"name": "TRV-Name",
|
||||
"local_name": "SBTR-001AEU",
|
||||
},
|
||||
"blutrv:201": {
|
||||
"id": 1,
|
||||
"enable": True,
|
||||
"min_valve_position": 0,
|
||||
"default_boost_duration": 1800,
|
||||
"default_override_duration": 2147483647,
|
||||
"default_override_target_C": 8,
|
||||
"addr": "f8:44:77:25:f0:de",
|
||||
"name": "TRV-201",
|
||||
"local_name": "SBTR-001AEU",
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
@@ -325,17 +287,6 @@ MOCK_BLU_TRV_REMOTE_STATUS = {
|
||||
"battery": 100,
|
||||
"errors": [],
|
||||
},
|
||||
"blutrv:201": {
|
||||
"id": 0,
|
||||
"pos": 0,
|
||||
"steps": 0,
|
||||
"current_C": 15.2,
|
||||
"target_C": 17.1,
|
||||
"schedule_rev": 0,
|
||||
"rssi": -60,
|
||||
"battery": 100,
|
||||
"errors": [],
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ from unittest.mock import AsyncMock, Mock, call, patch
|
||||
|
||||
from aioshelly.block_device import COAP
|
||||
from aioshelly.common import ConnectionOptions
|
||||
from aioshelly.const import MODEL_BLU_GATEWAY_G3, MODEL_PLUS_2PM
|
||||
from aioshelly.const import MODEL_PLUS_2PM
|
||||
from aioshelly.exceptions import (
|
||||
DeviceConnectionError,
|
||||
InvalidAuthError,
|
||||
@@ -38,7 +38,6 @@ from homeassistant.const import (
|
||||
from homeassistant.core import HomeAssistant
|
||||
from homeassistant.helpers import issue_registry as ir
|
||||
from homeassistant.helpers.device_registry import DeviceRegistry, format_mac
|
||||
from homeassistant.helpers.entity_registry import EntityRegistry
|
||||
from homeassistant.setup import async_setup_component
|
||||
|
||||
from . import MOCK_MAC, init_integration, mutate_rpc_device_status
|
||||
@@ -607,49 +606,3 @@ async def test_ble_scanner_unsupported_firmware_fixed(
|
||||
|
||||
assert not issue_registry.async_get_issue(DOMAIN, issue_id)
|
||||
assert len(issue_registry.issues) == 0
|
||||
|
||||
|
||||
async def test_blu_trv_stale_device_removal(
|
||||
hass: HomeAssistant,
|
||||
mock_blu_trv: Mock,
|
||||
entity_registry: EntityRegistry,
|
||||
device_registry: DeviceRegistry,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
"""Test BLU TRV removal of stale a device after un-pairing."""
|
||||
trv_200_entity_id = "climate.trv_name"
|
||||
trv_201_entity_id = "climate.trv_201"
|
||||
|
||||
monkeypatch.setattr(mock_blu_trv, "model", MODEL_BLU_GATEWAY_G3)
|
||||
gw_entry = await init_integration(hass, 3, model=MODEL_BLU_GATEWAY_G3)
|
||||
|
||||
# verify that both trv devices are present
|
||||
assert hass.states.get(trv_200_entity_id) is not None
|
||||
trv_200_entry = entity_registry.async_get(trv_200_entity_id)
|
||||
assert trv_200_entry
|
||||
|
||||
trv_200_device_entry = device_registry.async_get(trv_200_entry.device_id)
|
||||
assert trv_200_device_entry
|
||||
assert trv_200_device_entry.name == "TRV-Name"
|
||||
|
||||
assert hass.states.get(trv_201_entity_id) is not None
|
||||
trv_201_entry = entity_registry.async_get(trv_201_entity_id)
|
||||
assert trv_201_entry
|
||||
|
||||
trv_201_device_entry = device_registry.async_get(trv_201_entry.device_id)
|
||||
assert trv_201_device_entry
|
||||
assert trv_201_device_entry.name == "TRV-201"
|
||||
|
||||
# simulate un-pairing of trv 201 device
|
||||
monkeypatch.delitem(mock_blu_trv.config, "blutrv:201")
|
||||
monkeypatch.delitem(mock_blu_trv.status, "blutrv:201")
|
||||
|
||||
await hass.config_entries.async_reload(gw_entry.entry_id)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
# verify that trv 201 is removed
|
||||
assert hass.states.get(trv_200_entity_id) is not None
|
||||
assert device_registry.async_get(trv_200_entry.device_id) is not None
|
||||
|
||||
assert hass.states.get(trv_201_entity_id) is None
|
||||
assert device_registry.async_get(trv_201_entry.device_id) is None
|
||||
|
||||
@@ -19,8 +19,8 @@ from homeassistant.components.telegram_bot.const import (
|
||||
ERROR_MESSAGE,
|
||||
ISSUE_DEPRECATED_YAML,
|
||||
ISSUE_DEPRECATED_YAML_IMPORT_ISSUE_ERROR,
|
||||
PARSER_HTML,
|
||||
PARSER_MD,
|
||||
PARSER_PLAIN_TEXT,
|
||||
PLATFORM_BROADCAST,
|
||||
PLATFORM_WEBHOOKS,
|
||||
SUBENTRY_TYPE_ALLOWED_CHAT_IDS,
|
||||
@@ -56,13 +56,13 @@ async def test_options_flow(
|
||||
result = await hass.config_entries.options.async_configure(
|
||||
result["flow_id"],
|
||||
{
|
||||
ATTR_PARSER: PARSER_PLAIN_TEXT,
|
||||
ATTR_PARSER: PARSER_HTML,
|
||||
},
|
||||
)
|
||||
await hass.async_block_till_done()
|
||||
|
||||
assert result["type"] == FlowResultType.CREATE_ENTRY
|
||||
assert result["data"][ATTR_PARSER] is None
|
||||
assert result["data"][ATTR_PARSER] == PARSER_HTML
|
||||
|
||||
|
||||
async def test_reconfigure_flow_broadcast(
|
||||
|
||||
@@ -827,10 +827,12 @@ async def test_setup_source(hass: HomeAssistant) -> None:
|
||||
|
||||
assert entity.entity_sources(hass) == {
|
||||
"test_domain.platform_config_source": {
|
||||
"custom_component": False,
|
||||
"domain": "test_platform",
|
||||
},
|
||||
"test_domain.config_entry_source": {
|
||||
"config_entry": platform.config_entry.entry_id,
|
||||
"custom_component": False,
|
||||
"domain": "test_platform",
|
||||
},
|
||||
}
|
||||
|
||||
@@ -0,0 +1,41 @@
|
||||
"""Test backports package."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from enum import StrEnum
|
||||
from functools import cached_property # pylint: disable=hass-deprecated-import
|
||||
from types import ModuleType
|
||||
from typing import Any
|
||||
|
||||
import pytest
|
||||
|
||||
from homeassistant.backports import (
|
||||
enum as backports_enum,
|
||||
functools as backports_functools,
|
||||
)
|
||||
|
||||
from .common import import_and_test_deprecated_alias
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("module", "replacement", "breaks_in_ha_version"),
|
||||
[
|
||||
(backports_enum, StrEnum, "2025.5"),
|
||||
(backports_functools, cached_property, "2025.5"),
|
||||
],
|
||||
)
|
||||
def test_deprecated_aliases(
|
||||
caplog: pytest.LogCaptureFixture,
|
||||
module: ModuleType,
|
||||
replacement: Any,
|
||||
breaks_in_ha_version: str,
|
||||
) -> None:
|
||||
"""Test deprecated aliases."""
|
||||
alias_name = replacement.__name__
|
||||
import_and_test_deprecated_alias(
|
||||
caplog,
|
||||
module,
|
||||
alias_name,
|
||||
replacement,
|
||||
breaks_in_ha_version,
|
||||
)
|
||||
Reference in New Issue
Block a user