Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and this
## [1.8.4] - Unreleased

### Added
- Actionable HA research suggestions for Tuya, Reolink, Denon, Proxmox VE, go-e and original eWeLink devices; ESP modules receive an explicit firmware decision path. Weak name/vendor leads remain research with no claimed compatible integration, and community HACS sources are marked. Nine additional labels in all 20 languages.
- Offline Home Assistant integration choices per device, with captured evidence, reasons, prerequisites and explicit missing/conflicting information. The HA toolbar button prepares choices for every found device; device/service/history menus, JSON/CSV/HTML and PowerShell retain advice. New labels cover all 20 languages. No HA connection or automatic setup is performed.
- Slovenian labels for startup update checks and scrollbar page actions, preserving complete coverage of the upstream English dictionary.
- A unified Access list with exact captured HTTP/HTTPS endpoints, protocol/port suggestions, response and authentication evidence, and Open/Copy actions using existing clients. Deep web probes retain nonstandard endpoints; history, exports and PowerShell preserve the optional data. All 20 locales include the new labels.
- Device recognition evidence and heuristic confidence, source/timestamp details, explicitly reported model declarations, read-only device/service/history inspection, and structured evidence in JSON/CSV/HTML and PowerShell. Missing replies alone no longer imply IoT or Mobile; a MAC vendor never establishes a model.
Expand All @@ -16,6 +18,7 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) and this
- Per-tab scan options for neighbor cache, explicit ARP, ICMP, TCP discovery, DNS, mDNS, SSDP, port scanning and individual HTTP/TLS/SSH/SNMP/NetBIOS fingerprints. Profiles save the choices; scan, quick and deep paths enforce them in the engine. CLI `/skip` and PowerShell `-Skip` provide the same choices. Existing profiles and API calls keep their defaults.

### Fixed
- Discover mDNS/SSDP-only responders inside the requested targets and retain multicast evidence in quick scans. ARP also discovers silent on-link hosts; routed next hops are excluded. Preserve advertised SSDP device types and accept mDNS service-instance names for HA recommendations.
- Release Sync skips development forks because the checked marketplace channels belong to the upstream publisher; release API failures report the metadata lookup error instead of an empty tag (23 October 2026).
- The shortcuts card shows shadows at its edges when more content is out of view.

Expand Down
16 changes: 13 additions & 3 deletions Controls/HistoryWorkspace.Actions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,7 @@ void Add(string key, string gesture, string action, int glyph)
Add("Str_Ctx_CopyHost", "Ctrl+Alt+C", "host", 0xE8C8);
Add("Str_History_CopyDetails", "Ctrl+Shift+Y", "details", 0xE8C8);
Add("Str_Evidence_Title", "Ctrl+I", "evidence", 0xE946);
Add("Str_HA_Title", "", "ha", 0xE946);
menu.Items.Add(new Separator());
Add("Str_Export_Csv", "Ctrl+E", "export", 0xE896);
Add("Str_History_ViewChanges", "Ctrl+Shift+H", "changes", 0xE8FD);
Expand All @@ -65,7 +66,7 @@ void Add(string key, string gesture, string action, int glyph)
item.IsEnabled = action switch
{
"ip" or "mac" or "host" or "details" => grid != null && CopyValue(action).Length > 0,
"evidence" => grid != null && SelectedHistoryDevice != null,
"evidence" or "ha" => grid != null && SelectedHistoryDevice != null,
"export" => _entry != null && VisibleGrid.Items.Count > 0,
"changes" => _entry != null && _showAll,
"all" => _entry != null && !_showAll,
Expand Down Expand Up @@ -100,6 +101,15 @@ private string CopyValue(string action)

private async void ExecuteAction(string action)
{
if (action == "ha" && SelectedHistoryDevice is { } historical)
{
new HomeAssistantAdviceWindow(HomeAssistantText.FromHistory(historical), WebLink.Open, text =>
{
try { Clipboard.SetText(text); }
catch (COMException) { HistorySummary.Text = Loc("Str_Clipboard_Failed"); }
}) { Owner = Window.GetWindow(this) }.ShowDialog();
return;
}
if (action == "evidence" && SelectedHistoryDevice is { } selected)
{
new DeviceEvidenceWindow(new NetworkDevice { IpAddress = selected.IpAddress, Hostname = selected.Hostname,
Expand Down Expand Up @@ -127,15 +137,15 @@ private string BuildCsv()
{
static string Cell(string value) => ReportExport.CsvField(value);
var lines = new List<string> { string.Join(",", new[] { "Str_History_Change", "Str_Col_Ip", "Str_Col_DeviceName",
"Str_Col_Mac", "Str_Col_Vendor", "Str_Col_Type", "Str_Col_Ports" }.Select(key => Cell(Loc(key)))) + ",Location ID,Location,Identification JSON,Access JSON" };
"Str_Col_Mac", "Str_Col_Vendor", "Str_Col_Type", "Str_Col_Ports" }.Select(key => Cell(Loc(key)))) + ",Location ID,Location,Identification JSON,Access JSON,Home Assistant JSON" };
foreach (object row in VisibleGrid.Items)
{
var device = row is HistoryChangeRow change ? change.Device : ((HistoryDeviceRow)row).Device;
lines.Add(string.Join(",", new[] { row is HistoryChangeRow changed ? changed.Change : "",
device.IpAddress, device.Hostname, device.MacAddress, device.Vendor,
DeviceTypeConverter.Display(device.DeviceType), string.Join(", ", device.OpenPorts),
_entry?.LocationId ?? "", _entry?.LocationName ?? "",
device.Identification == null ? "" : System.Text.Json.JsonSerializer.Serialize(device.Identification), System.Text.Json.JsonSerializer.Serialize(device.Access) }.Select(Cell)));
device.Identification == null ? "" : System.Text.Json.JsonSerializer.Serialize(device.Identification), System.Text.Json.JsonSerializer.Serialize(device.Access), System.Text.Json.JsonSerializer.Serialize(HomeAssistantAdvisor.ForDevice(HomeAssistantText.FromHistory(device))) }.Select(Cell)));
}
return string.Join(Environment.NewLine, lines) + Environment.NewLine;
}
Expand Down
123 changes: 123 additions & 0 deletions Controls/HomeAssistantAdviceWindow.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
using System.Windows;
using System.Windows.Controls;
using KillerScan.Engine;
using KillerScan.Services;
using AccessText = KillerScan.Services.AccessText;

namespace KillerScan.Controls;

internal sealed class HomeAssistantAdviceWindow : Window
{
private readonly NetworkDevice _device;
private readonly Action<string> _open;
private readonly Action<string> _copy;
internal readonly TextBox IdentityInput = new() { MaxLength = 160 };
internal readonly ComboBox ProtocolInput = new();
internal readonly ComboBox GoalInput = new();
internal readonly StackPanel Results = new();
internal HomeAssistantAdvice CurrentAdvice { get; private set; }
private HomeAssistantContext CurrentContext => new()
{ ManufacturerModel = IdentityInput.Text, Protocol = ((ComboBoxItem?)ProtocolInput.SelectedItem)?.Tag as string ?? "",
Goal = ((ComboBoxItem?)GoalInput.SelectedItem)?.Tag as string ?? "Monitor" };

internal HomeAssistantAdviceWindow(NetworkDevice device, Action<string> open, Action<string> copy)
{
_device = HomeAssistantText.Freeze(device);
_open = open; _copy = copy; CurrentAdvice = HomeAssistantAdvisor.ForDevice(_device);
Title = HomeAssistantText.L("Title"); Width = 920; Height = 760; MinWidth = 540; MinHeight = 380;
WindowStartupLocation = WindowStartupLocation.CenterOwner;
Loaded += (_, _) => KillerFamily.Windowing.WorkAreaFit.Fit(this);
SetResourceReference(BackgroundProperty, "BackgroundBrush"); SetResourceReference(ForegroundProperty, "TextBrush"); SetResourceReference(FontFamilyProperty, "UiFont");
var panel = new DockPanel { Margin = new Thickness(18) };
var buttons = new WrapPanel { HorizontalAlignment = HorizontalAlignment.Right };
var catalogue = Button(HomeAssistantText.L("Docs")); catalogue.Tag = HomeAssistantAdvisor.CatalogueUrl;
catalogue.Click += (_, _) => _open(HomeAssistantAdvisor.CatalogueUrl);
var copyPlan = Button(HomeAssistantText.L("Copy")); copyPlan.Tag = "copy-plan";
copyPlan.Click += (_, _) => _copy(HomeAssistantText.Describe(_device, CurrentAdvice, CurrentContext));
var close = Button(Application.Current.TryFindResource("Str_Sys_Close") as string ?? "Close"); close.Click += (_, _) => Close();
buttons.Children.Add(catalogue); buttons.Children.Add(copyPlan); buttons.Children.Add(close);
DockPanel.SetDock(buttons, Dock.Bottom); panel.Children.Add(buttons);
var header = new StackPanel();
string deviceLabel = (device.IpAddress + " " + device.Hostname).Trim();
header.Children.Add(Text(deviceLabel.Length == 0 ? HomeAssistantText.L("Workflow") : deviceLabel, 18));
var inputs = new StackPanel();
inputs.Children.Add(Text(HomeAssistantText.L("Identity"))); IdentityInput.SetResourceReference(StyleProperty, "DarkTextBox");
System.Windows.Automation.AutomationProperties.SetName(IdentityInput, HomeAssistantText.L("Identity")); inputs.Children.Add(IdentityInput);
var fields = new WrapPanel();
fields.Children.Add(Field(HomeAssistantText.L("Protocol"), ProtocolInput));
ProtocolInput.Items.Add(new ComboBoxItem { Content = AccessText.L("Unknown"), Tag = "" });
foreach (string domain in new[] { "matter", "homekit_controller", "mqtt", "dlna_dmr", "rest", "snmp", "zha", "zwave_js", "bluetooth" })
ProtocolInput.Items.Add(new ComboBoxItem { Content = HomeAssistantAdvisor.Integration(domain)!.Name, Tag = domain });
ProtocolInput.SelectedIndex = 0;
fields.Children.Add(Field(HomeAssistantText.L("Goal"), GoalInput));
foreach (var (id, label) in new[] { ("Monitor", "Monitor"), ("Control", "ControlGoal"), ("Media", "MediaGoal") })
GoalInput.Items.Add(new ComboBoxItem { Content = HomeAssistantText.L(label), Tag = id });
GoalInput.SelectedIndex = 0; inputs.Children.Add(fields);
header.Children.Add(Expand(HomeAssistantText.L("Inputs"), inputs));
DockPanel.SetDock(header, Dock.Top); panel.Children.Add(header);
panel.Children.Add(new ScrollViewer { Content = Results, VerticalScrollBarVisibility = ScrollBarVisibility.Auto, HorizontalScrollBarVisibility = ScrollBarVisibility.Disabled });
Content = panel;
IdentityInput.TextChanged += (_, _) => Refresh(); ProtocolInput.SelectionChanged += (_, _) => Refresh(); GoalInput.SelectionChanged += (_, _) => Refresh();
Refresh();
}
private static FrameworkElement Field(string name, ComboBox input)
{
var field = new StackPanel { MinWidth = 230, Margin = new Thickness(0, 0, 14, 10) }; field.Children.Add(Text(name)); field.Children.Add(input);
System.Windows.Automation.AutomationProperties.SetName(input, name); return field;
}
private void Refresh()
{
var context = CurrentContext;
CurrentAdvice = context.ManufacturerModel.Length == 0 && context.Protocol.Length == 0 && context.Goal == "Monitor"
? HomeAssistantAdvisor.ForDevice(_device) : HomeAssistantAdvisor.Assess(_device, context);
Results.Children.Clear();
Results.Children.Add(Text(HomeAssistantText.L("BestChoice"), 14));
Results.Children.Add(Text(HomeAssistantText.Choice(CurrentAdvice), 24));
Results.Children.Add(Text(HomeAssistantText.Reason(CurrentAdvice)));
var best = HomeAssistantAdvisor.Best(CurrentAdvice) ?? HomeAssistantAdvisor.ResearchChoice(CurrentAdvice);
if (best != null)
{
var entry = HomeAssistantAdvisor.Integration(best.Domain)!;
Results.Children.Add(Text(HomeAssistantText.L(best.Basis)));
Results.Children.Add(Text(HomeAssistantText.L("Requirements") + ": " + string.Join("; ", best.Requirements.Select(HomeAssistantText.L))));
Results.Children.Add(Text(HomeAssistantText.Next(CurrentAdvice)));
if (CurrentAdvice.Steps.Contains("Control")) Results.Children.Add(Text(HomeAssistantText.L("Control")));
var docs = Button(HomeAssistantText.L("Docs") + " — " + entry.Name); docs.Tag = entry.DocumentationUrl;
docs.Click += (_, _) => _open(entry.DocumentationUrl); Results.Children.Add(docs);
}
Results.Children.Add(Text(HomeAssistantText.Catalogue(CurrentAdvice)));
Results.Children.Add(Text(HomeAssistantText.L("Note")));
var details = new StackPanel();
details.Children.Add(Text(HomeAssistantText.L("Fresh")));
foreach (var candidate in CurrentAdvice.Candidates)
{
var entry = HomeAssistantAdvisor.Integration(candidate.Domain);
if (entry == null) continue;
details.Children.Add(Text(entry.Name + " — " + HomeAssistantText.L(candidate.Basis), 17));
foreach (var fact in candidate.Evidence)
details.Children.Add(Text(fact.Source + " · " + HomeAssistantText.Time(fact.ObservedAtUtc) +
(fact.Reused ? " [" + IdentificationText.L("Cached") + "]" : "") + "\n" + fact.Value));
details.Children.Add(Text(HomeAssistantText.L("Requirements") + ": " + string.Join("; ", candidate.Requirements.Select(HomeAssistantText.L))));
if (candidate.Domain != best?.Domain)
{
var docs = Button(HomeAssistantText.L("Docs") + " — " + entry.Name); docs.Tag = entry.DocumentationUrl;
docs.Click += (_, _) => _open(entry.DocumentationUrl); details.Children.Add(docs);
}
}
details.Children.Add(Text(HomeAssistantText.L("Workflow"), 18));
for (int index = 0; index < CurrentAdvice.Steps.Length; index++) details.Children.Add(Text((index + 1) + ". " + HomeAssistantText.L(CurrentAdvice.Steps[index])));
var expander = Expand(HomeAssistantText.L("Details"), details); expander.IsExpanded = best == null; Results.Children.Add(expander);
}
private static Expander Expand(string header, object content)
{
var expander = new Expander { Header = header, Content = content, Margin = new Thickness(0, 8, 0, 10) };
expander.SetResourceReference(ForegroundProperty, "TextBrush"); return expander;
}
private static TextBlock Text(string value, double size = 13) => new()
{ Text = value, FontSize = size, TextWrapping = TextWrapping.Wrap, Margin = new Thickness(0, 5, 0, 7) };
private static Button Button(string text)
{
var button = new Button { Content = text, HorizontalAlignment = HorizontalAlignment.Left, Margin = new Thickness(0, 5, 10, 10), Padding = new Thickness(10, 5, 10, 5) };
button.SetResourceReference(StyleProperty, "OutlineButton"); return button;
}
}
Loading
Loading