diff --git a/src/nopywer/frontend/app.js b/src/nopywer/frontend/app.js
index 60039a4..c246419 100644
--- a/src/nopywer/frontend/app.js
+++ b/src/nopywer/frontend/app.js
@@ -96,6 +96,7 @@
}
}
+
function cableStyle(feature) {
const plugs = feature.properties && feature.properties.plugs_and_sockets_a
? feature.properties.plugs_and_sockets_a
@@ -109,59 +110,82 @@
}
return { color: "#f59e0b", weight: 4 };
}
+ function getCableWeight(plugs) {
+ if (plugs >= 63) return 8;
+ if (plugs >= 32) return 6;
+ return 4;
+ }
+ function getCableColor(plugs) {
+ if (plugs >= 63) return "#dc2626";
+ if (plugs >= 32) return "#ea580c";
+ return "#f59e0b";
+ }
+
+ function phaseColor(phase) {
+ const colors = { 0: "#6f2018dc", 1: "#000000", 2: "#9d9d9d" };
+ return colors[phase] ?? "#e81212";
+ }
function renderOptimizedCables(cablesGeojson) {
- optimizedCablesLayer.clearLayers();
+ console.log(cablesGeojson.features[0].properties);
+
+ optimizedCablesLayer.clearLayers();
+
+ const filter = function (feature) {
+ return feature.geometry && feature.geometry.type === "LineString";
+ };
+
+ // Capa base: tipo de cable (gruesa, semitransparente)
+ const baseLayer = L.geoJSON(cablesGeojson, {
+ filter,
+ style: function (feature) {
+ const style = cableStyle(feature);
+ return {
+ color: style.color,
+ weight: style.weight * 2,
+ opacity: 0.5,
+ };
+ },
+ });
- const cableLayer = L.geoJSON(cablesGeojson, {
- filter: function (feature) {
- return feature.geometry && feature.geometry.type === "LineString";
- },
- style: function (feature) {
- const style = cableStyle(feature);
- return {
- color: style.color,
- weight: style.weight,
- opacity: 0.95,
- };
- },
- onEachFeature: function (feature, layer) {
- const props = feature.properties || {};
- const details =
- "" +
- (props.from || "?") +
- " → " +
- (props.to || "?") +
- "
" +
- "Length: " +
- String(props.length_m ?? "?") +
- " m
" +
- "Cable: " +
- String(props.cable_type || "?") +
- "
" +
- "Current: " +
- String(props.current_a ?? "?") +
- " A
" +
- "Load: " +
- String(props.cum_power_kw ?? "?") +
- " kW";
-
- layer.bindPopup(details);
- layer.bindTooltip(details, {
- sticky: true,
- direction: "top",
- className: "cable-tooltip",
- });
- },
- });
+ // Capa superior: fase (fina, sólida) + popups
+ const phaseLayer = L.geoJSON(cablesGeojson, {
+ filter,
+ style: function (feature) {
+ const plugs = feature.properties?.plugs_and_sockets_a ?? 16;
+ const phase = feature.properties?.phase ?? null;
+ const baseWeight = cableStyle(feature).weight;
+ return {
+ color: phaseColor(phase),
+ weight: baseWeight,
+ opacity: 1,
+ };
+ },
+ onEachFeature: function (feature, layer) {
+ const props = feature.properties || {};
+ const phase = props.phase ?? null;
+ const plugs = props.plugs_and_sockets_a ?? 16;
+
+ const details =
+ "" + (props.from || "?") + " → " + (props.to || "?") + "
" +
+ "Phase: L" + ((phase ?? "?") + 1) + "
" +
+ "Cable: " + plugs + "A
" +
+ "Length: " + String(props.length_m ?? "?") + " m
" +
+ "Current: " + String(props.current_a ?? "?") + " A
" +
+ "Load: " + String(props.cum_power_kw ?? "?") + " kW";
+
+ layer.bindPopup(details);
+ },
+ });
- cableLayer.addTo(optimizedCablesLayer);
- if (cableLayer.getLayers().length > 0) {
- map.fitBounds(cableLayer.getBounds().pad(0.08));
- }
- bringNodesToFront();
- }
+ baseLayer.addTo(optimizedCablesLayer);
+ phaseLayer.addTo(optimizedCablesLayer);
+ if (phaseLayer.getLayers().length > 0) {
+ map.fitBounds(phaseLayer.getBounds().pad(0.08));
+ }
+ bringNodesToFront();
+}
function buildNodesGeojson() {
if (!nodesGeojson || !Array.isArray(nodesGeojson.features)) {
return { type: "FeatureCollection", features: [] };
@@ -254,3 +278,5 @@
map.setView([41.7008, -0.1379], 17);
});
})();
+
+console.log(cablesGeojson.features[0].properties);
diff --git a/src/nopywer/frontend/favicon.ico b/src/nopywer/frontend/favicon.ico
new file mode 100644
index 0000000..0e5b65f
Binary files /dev/null and b/src/nopywer/frontend/favicon.ico differ
diff --git a/src/nopywer/models.py b/src/nopywer/models.py
index 41a7454..136c9ef 100644
--- a/src/nopywer/models.py
+++ b/src/nopywer/models.py
@@ -14,7 +14,7 @@ class PowerNode:
lat: float
power_watts: float = 0.0
is_generator: bool = False
- phase: int | str | list | None = None
+ phase: int = 0
parent: str | None = None
children: dict[str, str] = field(default_factory=dict)
@@ -52,6 +52,7 @@ def to_geojson(self) -> dict:
"voltage": self.voltage,
"vdrop_percent": self.vdrop_percent,
"distro": self.distro,
+ "phase": self.phase,
},
}
@@ -62,7 +63,7 @@ class Cable:
length_m: float
area_mm2: float = 2.5
plugs_and_sockets_a: float = 16.0
- phase: int | str | list | None = None
+ phase: int = 0
from_node: str = ""
to_node: str = ""
@@ -113,6 +114,7 @@ def to_geojson(self) -> dict:
"current_a": round(max_current, 1),
"cum_power_kw": round(cum_power_w / 1000, 2),
"vdrop_volts": self.vdrop_volts,
+ "phase": self.phase,
},
}
@@ -124,6 +126,7 @@ class Cable16A(Cable):
tier_cost: ClassVar[float] = 1.0
num_phases: ClassVar[int] = 1
max_current_a: ClassVar[int] = 16
+ area_mm2: float = 2.5
@dataclass
diff --git a/telemetry data/telemetry visualizer.py b/telemetry data/telemetry visualizer.py
new file mode 100644
index 0000000..9f3b26f
--- /dev/null
+++ b/telemetry data/telemetry visualizer.py
@@ -0,0 +1,57 @@
+from pathlib import Path
+
+import pandas as pd
+import plotly.express as px
+
+
+def load_telemetry(file_path):
+ phases = {}
+ current_phase = None
+ rows = []
+
+ with open(file_path, encoding="utf-8-sig") as f:
+ for line in f:
+ line = line.strip()
+
+ if line.startswith("Phase"):
+ if current_phase and rows:
+ df_phase = pd.DataFrame(rows, columns=["Time", "Wh"])
+ phases[current_phase] = df_phase
+ rows = []
+ current_phase = line
+
+ elif line and not line.startswith("Time"):
+ parts = line.split(" , ")
+ if len(parts) == 2:
+ rows.append(parts)
+
+ if current_phase and rows:
+ df_phase = pd.DataFrame(rows, columns=["Time", "Wh"])
+ phases[current_phase] = df_phase
+ return phases
+
+
+def clean_phase_data(df):
+ df["Time"] = pd.to_datetime(df["Time"], dayfirst=True)
+ df["Wh"] = pd.to_numeric(df["Wh"])
+ return df
+
+
+DATA_DIR = Path("./telemetry data")
+files = list(DATA_DIR.glob("*.csv"))
+all_data = []
+
+for file in files:
+ phases = load_telemetry(file)
+
+ for phase_name, df in phases.items():
+ df = clean_phase_data(df)
+ df["phase"] = phase_name
+ df["source"] = file.name.split("_")[0]
+ all_data.append(df)
+
+df = pd.concat(all_data)
+
+fig = px.line(df, x="Time", y="Wh", color="phase", facet_row="source", title="Power telemetry")
+
+fig.show()